Merge with master
|
@ -11,4 +11,4 @@ exclude_lines =
|
||||||
if __name__ == .__main__.:
|
if __name__ == .__main__.:
|
||||||
# Don't complain about debug code
|
# Don't complain about debug code
|
||||||
if Image.DEBUG:
|
if Image.DEBUG:
|
||||||
if DEBUG:
|
if DEBUG:
|
||||||
|
|
6
.gitignore
vendored
|
@ -60,3 +60,9 @@ docs/_build/
|
||||||
\#*#
|
\#*#
|
||||||
.#*
|
.#*
|
||||||
|
|
||||||
|
#Komodo
|
||||||
|
*.komodoproject
|
||||||
|
|
||||||
|
#OS
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
|
45
.travis.yml
|
@ -7,17 +7,23 @@ env: MAX_CONCURRENCY=4
|
||||||
|
|
||||||
python:
|
python:
|
||||||
- "pypy"
|
- "pypy"
|
||||||
|
- "pypy3"
|
||||||
- 2.6
|
- 2.6
|
||||||
- 2.7
|
- 2.7
|
||||||
|
- "2.7_with_system_site_packages" # For PyQt4
|
||||||
- 3.2
|
- 3.2
|
||||||
- 3.3
|
- 3.3
|
||||||
- 3.4
|
- 3.4
|
||||||
|
|
||||||
install:
|
install:
|
||||||
- "sudo apt-get -qq install libfreetype6-dev liblcms2-dev python-qt4 ghostscript libffi-dev libjpeg-turbo-progs cmake imagemagick"
|
- "travis_retry sudo apt-get -qq install libfreetype6-dev liblcms2-dev python-qt4 ghostscript libffi-dev libjpeg-turbo-progs cmake imagemagick"
|
||||||
- "pip install cffi"
|
- "travis_retry pip install cffi"
|
||||||
- "pip install coveralls nose pyroma"
|
- "travis_retry pip install coverage nose"
|
||||||
- if [ "$TRAVIS_PYTHON_VERSION" == "2.6" ]; then pip install unittest2; fi
|
|
||||||
|
# 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
|
# webp
|
||||||
- pushd depends && ./install_webp.sh && popd
|
- pushd depends && ./install_webp.sh && popd
|
||||||
|
@ -28,22 +34,27 @@ install:
|
||||||
script:
|
script:
|
||||||
- coverage erase
|
- coverage erase
|
||||||
- python setup.py clean
|
- 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)
|
- coverage run --append --include=PIL/* selftest.py
|
||||||
- if [ "$TRAVIS_PYTHON_VERSION" == "pypy" ]; then time python selftest.py; fi
|
- coverage run --append --include=PIL/* -m nose -vx Tests/test_*.py
|
||||||
- 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
|
|
||||||
|
|
||||||
after_success:
|
after_success:
|
||||||
- coverage report
|
# gather the coverage data
|
||||||
# No need to send empty coverage to Coveralls for PyPy
|
- travis_retry sudo apt-get -qq install lcov
|
||||||
- if [ "$TRAVIS_PYTHON_VERSION" != "pypy" ]; then coveralls; fi
|
- 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 PIL/*.py
|
||||||
- pep8 --statistics --count Tests/*.py
|
- pep8 --statistics --count Tests/*.py
|
||||||
- pyflakes PIL/*.py | tee >(wc -l)
|
- pyflakes PIL/*.py | tee >(wc -l)
|
||||||
|
@ -54,3 +65,5 @@ after_success:
|
||||||
# (Installation is very slow on Py3, so just do it for Py2.)
|
# (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-install.sh; fi
|
||||||
- if [ ${TRAVIS_PYTHON_VERSION:0:1} == "2" ]; then Scripts/diffcover-run.sh; fi
|
- if [ ${TRAVIS_PYTHON_VERSION:0:1} == "2" ]; then Scripts/diffcover-run.sh; fi
|
||||||
|
matrix:
|
||||||
|
fast_finish: true
|
||||||
|
|
114
CHANGES.rst
|
@ -4,9 +4,115 @@ Changelog (Pillow)
|
||||||
2.6.0 (unreleased)
|
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]
|
[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)
|
2.5.0 (2014-07-01)
|
||||||
------------------
|
------------------
|
||||||
|
@ -227,6 +333,12 @@ Changelog (Pillow)
|
||||||
- Prefer homebrew freetype over X11 freetype (but still allow both)
|
- Prefer homebrew freetype over X11 freetype (but still allow both)
|
||||||
[dmckeone]
|
[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)
|
2.3.1 (2014-03-14)
|
||||||
------------------
|
------------------
|
||||||
|
|
||||||
|
|
26
CONTRIBUTING.md
Normal file
|
@ -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?
|
22
MANIFEST.in
|
@ -1,5 +1,7 @@
|
||||||
|
|
||||||
include *.c
|
include *.c
|
||||||
include *.h
|
include *.h
|
||||||
|
include *.md
|
||||||
include *.py
|
include *.py
|
||||||
include *.rst
|
include *.rst
|
||||||
include *.txt
|
include *.txt
|
||||||
|
@ -25,14 +27,20 @@ recursive-include Images *.xpm
|
||||||
recursive-include PIL *.md
|
recursive-include PIL *.md
|
||||||
recursive-include Sane *.c
|
recursive-include Sane *.c
|
||||||
recursive-include Sane *.py
|
recursive-include Sane *.py
|
||||||
|
recursive-include Sane *.rst
|
||||||
recursive-include Sane *.txt
|
recursive-include Sane *.txt
|
||||||
recursive-include Sane CHANGES
|
recursive-include Sane CHANGES
|
||||||
recursive-include Sane README
|
recursive-include Sane README
|
||||||
recursive-include Scripts *.py
|
recursive-include Scripts *.py
|
||||||
|
recursive-include Scripts *.rst
|
||||||
|
recursive-include Scripts *.sh
|
||||||
recursive-include Scripts README
|
recursive-include Scripts README
|
||||||
recursive-include Tests *.bdf
|
recursive-include Tests *.bdf
|
||||||
recursive-include Tests *.bin
|
recursive-include Tests *.bin
|
||||||
recursive-include Tests *.bmp
|
recursive-include Tests *.bmp
|
||||||
|
recursive-include Tests *.bw
|
||||||
|
recursive-include Tests *.cur
|
||||||
|
recursive-include Tests *.dcx
|
||||||
recursive-include Tests *.doc
|
recursive-include Tests *.doc
|
||||||
recursive-include Tests *.eps
|
recursive-include Tests *.eps
|
||||||
recursive-include Tests *.fli
|
recursive-include Tests *.fli
|
||||||
|
@ -46,6 +54,7 @@ recursive-include Tests *.j2k
|
||||||
recursive-include Tests *.jp2
|
recursive-include Tests *.jp2
|
||||||
recursive-include Tests *.jpg
|
recursive-include Tests *.jpg
|
||||||
recursive-include Tests *.lut
|
recursive-include Tests *.lut
|
||||||
|
recursive-include Tests *.mpo
|
||||||
recursive-include Tests *.pbm
|
recursive-include Tests *.pbm
|
||||||
recursive-include Tests *.pcf
|
recursive-include Tests *.pcf
|
||||||
recursive-include Tests *.pcx
|
recursive-include Tests *.pcx
|
||||||
|
@ -55,7 +64,10 @@ recursive-include Tests *.png
|
||||||
recursive-include Tests *.ppm
|
recursive-include Tests *.ppm
|
||||||
recursive-include Tests *.psd
|
recursive-include Tests *.psd
|
||||||
recursive-include Tests *.py
|
recursive-include Tests *.py
|
||||||
|
recursive-include Tests *.ras
|
||||||
|
recursive-include Tests *.rgb
|
||||||
recursive-include Tests *.rst
|
recursive-include Tests *.rst
|
||||||
|
recursive-include Tests *.sgi
|
||||||
recursive-include Tests *.spider
|
recursive-include Tests *.spider
|
||||||
recursive-include Tests *.tar
|
recursive-include Tests *.tar
|
||||||
recursive-include Tests *.tif
|
recursive-include Tests *.tif
|
||||||
|
@ -65,22 +77,20 @@ recursive-include Tests *.txt
|
||||||
recursive-include Tests *.webp
|
recursive-include Tests *.webp
|
||||||
recursive-include Tests *.xpm
|
recursive-include Tests *.xpm
|
||||||
recursive-include Tk *.c
|
recursive-include Tk *.c
|
||||||
recursive-include Tk *.txt
|
|
||||||
recursive-include Tk *.rst
|
recursive-include Tk *.rst
|
||||||
recursive-include depends *.sh
|
recursive-include Tk *.txt
|
||||||
recursive-include depends *.rst
|
recursive-include depends *.rst
|
||||||
|
recursive-include depends *.sh
|
||||||
recursive-include docs *.bat
|
recursive-include docs *.bat
|
||||||
recursive-include docs *.gitignore
|
recursive-include docs *.gitignore
|
||||||
recursive-include docs *.html
|
recursive-include docs *.html
|
||||||
recursive-include docs *.py
|
recursive-include docs *.py
|
||||||
recursive-include docs *.rst
|
recursive-include docs *.rst
|
||||||
recursive-include docs *.txt
|
recursive-include docs *.txt
|
||||||
recursive-include docs Guardfile
|
|
||||||
recursive-include docs Makefile
|
|
||||||
recursive-include docs BUILDME
|
recursive-include docs BUILDME
|
||||||
recursive-include docs COPYING
|
recursive-include docs COPYING
|
||||||
|
recursive-include docs Guardfile
|
||||||
recursive-include docs LICENSE
|
recursive-include docs LICENSE
|
||||||
|
recursive-include docs Makefile
|
||||||
recursive-include libImaging *.c
|
recursive-include libImaging *.c
|
||||||
recursive-include libImaging *.h
|
recursive-include libImaging *.h
|
||||||
recursive-include Sane *.rst
|
|
||||||
recursive-include Scripts *.rst
|
|
||||||
|
|
23
Makefile
|
@ -1,4 +1,16 @@
|
||||||
|
.PHONY: pre clean install test inplace coverage test-dep help docs livedocs
|
||||||
|
|
||||||
|
help:
|
||||||
|
@echo "Please use \`make <target>' where <target> 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:
|
pre:
|
||||||
virtualenv .
|
virtualenv .
|
||||||
|
@ -15,13 +27,14 @@ pre:
|
||||||
clean:
|
clean:
|
||||||
python setup.py clean
|
python setup.py clean
|
||||||
rm PIL/*.so || true
|
rm PIL/*.so || true
|
||||||
find . -name __pycache__ | xargs rm -r
|
rm -r build || true
|
||||||
|
find . -name __pycache__ | xargs rm -r || true
|
||||||
|
|
||||||
install:
|
install:
|
||||||
python setup.py install
|
python setup.py install
|
||||||
python selftest.py --installed
|
python selftest.py --installed
|
||||||
|
|
||||||
test: install
|
test:
|
||||||
python test-installed.py
|
python test-installed.py
|
||||||
|
|
||||||
inplace: clean
|
inplace: clean
|
||||||
|
@ -40,3 +53,9 @@ coverage:
|
||||||
|
|
||||||
test-dep:
|
test-dep:
|
||||||
pip install coveralls nose nose-cov pep8 pyflakes
|
pip install coveralls nose nose-cov pep8 pyflakes
|
||||||
|
|
||||||
|
docs:
|
||||||
|
$(MAKE) -C docs html
|
||||||
|
|
||||||
|
docserver:
|
||||||
|
cd docs/_build/html && python -mSimpleHTTPServer 2> /dev/null&
|
|
@ -33,6 +33,7 @@ i32 = _binary.i32le
|
||||||
def _accept(prefix):
|
def _accept(prefix):
|
||||||
return prefix[:4] == b"\0\0\2\0"
|
return prefix[:4] == b"\0\0\2\0"
|
||||||
|
|
||||||
|
|
||||||
##
|
##
|
||||||
# Image plugin for Windows Cursor files.
|
# Image plugin for Windows Cursor files.
|
||||||
|
|
||||||
|
@ -48,7 +49,7 @@ class CurImageFile(BmpImagePlugin.BmpImageFile):
|
||||||
# check magic
|
# check magic
|
||||||
s = self.fp.read(6)
|
s = self.fp.read(6)
|
||||||
if not _accept(s):
|
if not _accept(s):
|
||||||
raise SyntaxError("not an CUR file")
|
raise SyntaxError("not a CUR file")
|
||||||
|
|
||||||
# pick the largest cursor in the file
|
# pick the largest cursor in the file
|
||||||
m = b""
|
m = b""
|
||||||
|
@ -58,14 +59,14 @@ class CurImageFile(BmpImagePlugin.BmpImageFile):
|
||||||
m = s
|
m = s
|
||||||
elif i8(s[0]) > i8(m[0]) and i8(s[1]) > i8(m[1]):
|
elif i8(s[0]) > i8(m[0]) and i8(s[1]) > i8(m[1]):
|
||||||
m = s
|
m = s
|
||||||
#print "width", i8(s[0])
|
# print "width", i8(s[0])
|
||||||
#print "height", i8(s[1])
|
# print "height", i8(s[1])
|
||||||
#print "colors", i8(s[2])
|
# print "colors", i8(s[2])
|
||||||
#print "reserved", i8(s[3])
|
# print "reserved", i8(s[3])
|
||||||
#print "hotspot x", i16(s[4:])
|
# print "hotspot x", i16(s[4:])
|
||||||
#print "hotspot y", i16(s[6:])
|
# print "hotspot y", i16(s[6:])
|
||||||
#print "bytes", i32(s[8:])
|
# print "bytes", i32(s[8:])
|
||||||
#print "offset", i32(s[12:])
|
# print "offset", i32(s[12:])
|
||||||
|
|
||||||
# load as bitmap
|
# load as bitmap
|
||||||
self._bitmap(i32(m[12:]) + offset)
|
self._bitmap(i32(m[12:]) + offset)
|
||||||
|
@ -73,7 +74,7 @@ class CurImageFile(BmpImagePlugin.BmpImageFile):
|
||||||
# patch up the bitmap height
|
# patch up the bitmap height
|
||||||
self.size = self.size[0], self.size[1]//2
|
self.size = self.size[0], self.size[1]//2
|
||||||
d, e, o, a = self.tile[0]
|
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
|
return
|
||||||
|
|
||||||
|
|
|
@ -27,13 +27,15 @@ from PIL import Image, _binary
|
||||||
|
|
||||||
from PIL.PcxImagePlugin import PcxImageFile
|
from PIL.PcxImagePlugin import PcxImageFile
|
||||||
|
|
||||||
MAGIC = 0x3ADE68B1 # QUIZ: what's this value, then?
|
MAGIC = 0x3ADE68B1 # QUIZ: what's this value, then?
|
||||||
|
|
||||||
i32 = _binary.i32le
|
i32 = _binary.i32le
|
||||||
|
|
||||||
|
|
||||||
def _accept(prefix):
|
def _accept(prefix):
|
||||||
return i32(prefix) == MAGIC
|
return i32(prefix) == MAGIC
|
||||||
|
|
||||||
|
|
||||||
##
|
##
|
||||||
# Image plugin for the Intel DCX format.
|
# Image plugin for the Intel DCX format.
|
||||||
|
|
||||||
|
|
|
@ -86,26 +86,32 @@ def Ghostscript(tile, size, fp, scale=1):
|
||||||
|
|
||||||
out_fd, outfile = tempfile.mkstemp()
|
out_fd, outfile = tempfile.mkstemp()
|
||||||
os.close(out_fd)
|
os.close(out_fd)
|
||||||
in_fd, infile = tempfile.mkstemp()
|
|
||||||
os.close(in_fd)
|
infile_temp = None
|
||||||
|
if hasattr(fp, 'name') and os.path.exists(fp.name):
|
||||||
# ignore length and offset!
|
infile = fp.name
|
||||||
# ghostscript can read it
|
else:
|
||||||
# copy whole file to read in ghostscript
|
in_fd, infile_temp = tempfile.mkstemp()
|
||||||
with open(infile, 'wb') as f:
|
os.close(in_fd)
|
||||||
# fetch length of fp
|
infile = infile_temp
|
||||||
fp.seek(0, 2)
|
|
||||||
fsize = fp.tell()
|
# ignore length and offset!
|
||||||
# ensure start position
|
# ghostscript can read it
|
||||||
# go back
|
# copy whole file to read in ghostscript
|
||||||
fp.seek(0)
|
with open(infile_temp, 'wb') as f:
|
||||||
lengthfile = fsize
|
# fetch length of fp
|
||||||
while lengthfile > 0:
|
fp.seek(0, 2)
|
||||||
s = fp.read(min(lengthfile, 100*1024))
|
fsize = fp.tell()
|
||||||
if not s:
|
# ensure start position
|
||||||
break
|
# go back
|
||||||
length -= len(s)
|
fp.seek(0)
|
||||||
f.write(s)
|
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
|
# Build ghostscript command
|
||||||
command = ["gs",
|
command = ["gs",
|
||||||
|
@ -136,49 +142,36 @@ def Ghostscript(tile, size, fp, scale=1):
|
||||||
finally:
|
finally:
|
||||||
try:
|
try:
|
||||||
os.unlink(outfile)
|
os.unlink(outfile)
|
||||||
os.unlink(infile)
|
if infile_temo:
|
||||||
|
os.unlink(infile_temp)
|
||||||
except: pass
|
except: pass
|
||||||
|
|
||||||
return im
|
return im
|
||||||
|
|
||||||
|
|
||||||
class PSFile:
|
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):
|
def __init__(self, fp):
|
||||||
self.fp = fp
|
self.fp = fp
|
||||||
self.char = None
|
self.char = None
|
||||||
def __getattr__(self, id):
|
|
||||||
v = getattr(self.fp, id)
|
|
||||||
setattr(self, id, v)
|
|
||||||
return v
|
|
||||||
def seek(self, offset, whence=0):
|
def seek(self, offset, whence=0):
|
||||||
self.char = None
|
self.char = None
|
||||||
self.fp.seek(offset, whence)
|
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):
|
def readline(self):
|
||||||
s = b""
|
s = self.char or b""
|
||||||
if self.char:
|
self.char = None
|
||||||
c = self.char
|
|
||||||
self.char = None
|
c = self.fp.read(1)
|
||||||
else:
|
|
||||||
c = self.fp.read(1)
|
|
||||||
while c not in b"\r\n":
|
while c not in b"\r\n":
|
||||||
s = s + c
|
s = s + c
|
||||||
c = self.fp.read(1)
|
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):
|
def _accept(prefix):
|
||||||
return prefix[:4] == b"%!PS" or i32(prefix) == 0xC6D3D0C5
|
return prefix[:4] == b"%!PS" or i32(prefix) == 0xC6D3D0C5
|
||||||
|
@ -193,36 +186,27 @@ class EpsImageFile(ImageFile.ImageFile):
|
||||||
format = "EPS"
|
format = "EPS"
|
||||||
format_description = "Encapsulated Postscript"
|
format_description = "Encapsulated Postscript"
|
||||||
|
|
||||||
|
mode_map = { 1:"L", 2:"LAB", 3:"RGB" }
|
||||||
|
|
||||||
def _open(self):
|
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
|
# go to offset - start of "%!PS"
|
||||||
# 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"
|
|
||||||
fp.seek(offset)
|
fp.seek(offset)
|
||||||
|
|
||||||
box = None
|
box = None
|
||||||
|
|
||||||
self.mode = "RGB"
|
self.mode = "RGB"
|
||||||
|
@ -231,18 +215,12 @@ class EpsImageFile(ImageFile.ImageFile):
|
||||||
#
|
#
|
||||||
# Load EPS header
|
# Load EPS header
|
||||||
|
|
||||||
s = fp.readline()
|
s = fp.readline().strip('\r\n')
|
||||||
|
|
||||||
while s:
|
while s:
|
||||||
|
|
||||||
if len(s) > 255:
|
if len(s) > 255:
|
||||||
raise SyntaxError("not an EPS file")
|
raise SyntaxError("not an EPS file")
|
||||||
|
|
||||||
if s[-2:] == '\r\n':
|
|
||||||
s = s[:-2]
|
|
||||||
elif s[-1:] == '\n':
|
|
||||||
s = s[:-1]
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
m = split.match(s)
|
m = split.match(s)
|
||||||
except re.error as v:
|
except re.error as v:
|
||||||
|
@ -264,9 +242,7 @@ class EpsImageFile(ImageFile.ImageFile):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
else:
|
else:
|
||||||
|
|
||||||
m = field.match(s)
|
m = field.match(s)
|
||||||
|
|
||||||
if m:
|
if m:
|
||||||
k = m.group(1)
|
k = m.group(1)
|
||||||
|
|
||||||
|
@ -276,16 +252,16 @@ class EpsImageFile(ImageFile.ImageFile):
|
||||||
self.info[k[:8]] = k[9:]
|
self.info[k[:8]] = k[9:]
|
||||||
else:
|
else:
|
||||||
self.info[k] = ""
|
self.info[k] = ""
|
||||||
elif s[0:1] == '%':
|
elif s[0] == '%':
|
||||||
# handle non-DSC Postscript comments that some
|
# handle non-DSC Postscript comments that some
|
||||||
# tools mistakenly put in the Comments section
|
# tools mistakenly put in the Comments section
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
raise IOError("bad EPS header")
|
raise IOError("bad EPS header")
|
||||||
|
|
||||||
s = fp.readline()
|
s = fp.readline().strip('\r\n')
|
||||||
|
|
||||||
if s[:1] != "%":
|
if s[0] != "%":
|
||||||
break
|
break
|
||||||
|
|
||||||
|
|
||||||
|
@ -297,63 +273,48 @@ class EpsImageFile(ImageFile.ImageFile):
|
||||||
if len(s) > 255:
|
if len(s) > 255:
|
||||||
raise SyntaxError("not an EPS file")
|
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:":
|
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] =\
|
if int(bi) != 8:
|
||||||
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:
|
|
||||||
break
|
break
|
||||||
if bi != 8:
|
try:
|
||||||
|
self.mode = self.mode_map[int(mo)]
|
||||||
|
except:
|
||||||
break
|
break
|
||||||
if mo == 1:
|
|
||||||
self.mode = "L"
|
self.size = int(x), int(y)
|
||||||
elif mo == 2:
|
return
|
||||||
self.mode = "LAB"
|
|
||||||
elif mo == 3:
|
s = fp.readline().strip('\r\n')
|
||||||
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()
|
|
||||||
if not s:
|
if not s:
|
||||||
break
|
break
|
||||||
|
|
||||||
if not box:
|
if not box:
|
||||||
raise IOError("cannot determine EPS bounding 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):
|
def load(self, scale=1):
|
||||||
# Load EPS via Ghostscript
|
# Load EPS via Ghostscript
|
||||||
if not self.tile:
|
if not self.tile:
|
||||||
|
|
|
@ -67,8 +67,8 @@ TAGS = {
|
||||||
0x0213: "YCbCrPositioning",
|
0x0213: "YCbCrPositioning",
|
||||||
0x0214: "ReferenceBlackWhite",
|
0x0214: "ReferenceBlackWhite",
|
||||||
0x1000: "RelatedImageFileFormat",
|
0x1000: "RelatedImageFileFormat",
|
||||||
0x1001: "RelatedImageLength", # FIXME / Dictionary contains duplicate keys
|
0x1001: "RelatedImageWidth",
|
||||||
0x1001: "RelatedImageWidth", # FIXME \ Dictionary contains duplicate keys
|
0x1002: "RelatedImageLength",
|
||||||
0x828d: "CFARepeatPatternDim",
|
0x828d: "CFARepeatPatternDim",
|
||||||
0x828e: "CFAPattern",
|
0x828e: "CFAPattern",
|
||||||
0x828f: "BatteryLevel",
|
0x828f: "BatteryLevel",
|
||||||
|
|
|
@ -17,8 +17,6 @@
|
||||||
import os
|
import os
|
||||||
from PIL import Image, _binary
|
from PIL import Image, _binary
|
||||||
|
|
||||||
import marshal
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import zlib
|
import zlib
|
||||||
except ImportError:
|
except ImportError:
|
||||||
|
@ -26,6 +24,7 @@ except ImportError:
|
||||||
|
|
||||||
WIDTH = 800
|
WIDTH = 800
|
||||||
|
|
||||||
|
|
||||||
def puti16(fp, values):
|
def puti16(fp, values):
|
||||||
# write network order (big-endian) 16-bit sequence
|
# write network order (big-endian) 16-bit sequence
|
||||||
for v in values:
|
for v in values:
|
||||||
|
@ -33,6 +32,7 @@ def puti16(fp, values):
|
||||||
v += 65536
|
v += 65536
|
||||||
fp.write(_binary.o16be(v))
|
fp.write(_binary.o16be(v))
|
||||||
|
|
||||||
|
|
||||||
##
|
##
|
||||||
# Base class for raster font file handlers.
|
# Base class for raster font file handlers.
|
||||||
|
|
||||||
|
@ -95,9 +95,8 @@ class FontFile:
|
||||||
# print chr(i), dst, s
|
# print chr(i), dst, s
|
||||||
self.metrics[i] = d, dst, s
|
self.metrics[i] = d, dst, s
|
||||||
|
|
||||||
|
def save(self, filename):
|
||||||
def save1(self, filename):
|
"Save font"
|
||||||
"Save font in version 1 format"
|
|
||||||
|
|
||||||
self.compile()
|
self.compile()
|
||||||
|
|
||||||
|
@ -107,7 +106,7 @@ class FontFile:
|
||||||
# font metrics
|
# font metrics
|
||||||
fp = open(os.path.splitext(filename)[0] + ".pil", "wb")
|
fp = open(os.path.splitext(filename)[0] + ".pil", "wb")
|
||||||
fp.write(b"PILfont\n")
|
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")
|
fp.write(b"DATA\n")
|
||||||
for id in range(256):
|
for id in range(256):
|
||||||
m = self.metrics[id]
|
m = self.metrics[id]
|
||||||
|
@ -117,30 +116,4 @@ class FontFile:
|
||||||
puti16(fp, m[0] + m[1] + m[2])
|
puti16(fp, m[0] + m[1] + m[2])
|
||||||
fp.close()
|
fp.close()
|
||||||
|
|
||||||
|
# End of file
|
||||||
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
|
|
||||||
|
|
|
@ -96,8 +96,15 @@ class GifImageFile(ImageFile.ImageFile):
|
||||||
# rewind
|
# rewind
|
||||||
self.__offset = 0
|
self.__offset = 0
|
||||||
self.dispose = None
|
self.dispose = None
|
||||||
|
self.dispose_extent = [0, 0, 0, 0] #x0, y0, x1, y1
|
||||||
self.__frame = -1
|
self.__frame = -1
|
||||||
self.__fp.seek(self.__rewind)
|
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:
|
if frame != self.__frame + 1:
|
||||||
raise ValueError("cannot seek to frame %d" % frame)
|
raise ValueError("cannot seek to frame %d" % frame)
|
||||||
|
@ -114,8 +121,7 @@ class GifImageFile(ImageFile.ImageFile):
|
||||||
self.__offset = 0
|
self.__offset = 0
|
||||||
|
|
||||||
if self.dispose:
|
if self.dispose:
|
||||||
self.im = self.dispose
|
self.im.paste(self.dispose, self.dispose_extent)
|
||||||
self.dispose = None
|
|
||||||
|
|
||||||
from copy import copy
|
from copy import copy
|
||||||
self.palette = copy(self.global_palette)
|
self.palette = copy(self.global_palette)
|
||||||
|
@ -140,17 +146,16 @@ class GifImageFile(ImageFile.ImageFile):
|
||||||
if flags & 1:
|
if flags & 1:
|
||||||
self.info["transparency"] = i8(block[3])
|
self.info["transparency"] = i8(block[3])
|
||||||
self.info["duration"] = i16(block[1:3]) * 10
|
self.info["duration"] = i16(block[1:3]) * 10
|
||||||
try:
|
|
||||||
# disposal methods
|
# disposal method - find the value of bits 4 - 6
|
||||||
if flags & 8:
|
dispose_bits = 0b00011100 & flags
|
||||||
# replace with background colour
|
dispose_bits = dispose_bits >> 2
|
||||||
self.dispose = Image.core.fill("P", self.size,
|
if dispose_bits:
|
||||||
self.info["background"])
|
# only set the dispose if it is not
|
||||||
elif flags & 16:
|
# unspecified. I'm not sure if this is
|
||||||
# replace with previous contents
|
# correct, but it seems to prevent the last
|
||||||
self.dispose = self.im.copy()
|
# frame from looking odd for some animations
|
||||||
except (AttributeError, KeyError):
|
self.disposal_method = dispose_bits
|
||||||
pass
|
|
||||||
elif i8(s) == 255:
|
elif i8(s) == 255:
|
||||||
#
|
#
|
||||||
# application extension
|
# application extension
|
||||||
|
@ -172,6 +177,7 @@ class GifImageFile(ImageFile.ImageFile):
|
||||||
# extent
|
# extent
|
||||||
x0, y0 = i16(s[0:]), i16(s[2:])
|
x0, y0 = i16(s[0:]), i16(s[2:])
|
||||||
x1, y1 = x0 + i16(s[4:]), y0 + i16(s[6:])
|
x1, y1 = x0 + i16(s[4:]), y0 + i16(s[6:])
|
||||||
|
self.dispose_extent = x0, y0, x1, y1
|
||||||
flags = i8(s[8])
|
flags = i8(s[8])
|
||||||
|
|
||||||
interlace = (flags & 64) != 0
|
interlace = (flags & 64) != 0
|
||||||
|
@ -194,6 +200,26 @@ class GifImageFile(ImageFile.ImageFile):
|
||||||
pass
|
pass
|
||||||
# raise IOError, "illegal GIF tag `%x`" % i8(s)
|
# 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:
|
if not self.tile:
|
||||||
# self.__fp = None
|
# self.__fp = None
|
||||||
raise EOFError("no more images in GIF file")
|
raise EOFError("no more images in GIF file")
|
||||||
|
@ -205,6 +231,18 @@ class GifImageFile(ImageFile.ImageFile):
|
||||||
def tell(self):
|
def tell(self):
|
||||||
return self.__frame
|
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
|
# Write GIF files
|
||||||
|
@ -230,10 +268,9 @@ def _save(im, fp, filename):
|
||||||
except IOError:
|
except IOError:
|
||||||
pass # write uncompressed file
|
pass # write uncompressed file
|
||||||
|
|
||||||
try:
|
if im.mode in RAWMODE:
|
||||||
rawmode = RAWMODE[im.mode]
|
|
||||||
imOut = im
|
imOut = im
|
||||||
except KeyError:
|
else:
|
||||||
# convert on the fly (EXPERIMENTAL -- I'm not sure PIL
|
# convert on the fly (EXPERIMENTAL -- I'm not sure PIL
|
||||||
# should automatically convert images on save...)
|
# should automatically convert images on save...)
|
||||||
if Image.getmodebase(im.mode) == "RGB":
|
if Image.getmodebase(im.mode) == "RGB":
|
||||||
|
@ -241,10 +278,8 @@ def _save(im, fp, filename):
|
||||||
if im.palette:
|
if im.palette:
|
||||||
palette_size = len(im.palette.getdata()[1]) // 3
|
palette_size = len(im.palette.getdata()[1]) // 3
|
||||||
imOut = im.convert("P", palette=1, colors=palette_size)
|
imOut = im.convert("P", palette=1, colors=palette_size)
|
||||||
rawmode = "P"
|
|
||||||
else:
|
else:
|
||||||
imOut = im.convert("L")
|
imOut = im.convert("L")
|
||||||
rawmode = "L"
|
|
||||||
|
|
||||||
# header
|
# header
|
||||||
try:
|
try:
|
||||||
|
@ -252,12 +287,6 @@ def _save(im, fp, filename):
|
||||||
except KeyError:
|
except KeyError:
|
||||||
palette = None
|
palette = None
|
||||||
im.encoderinfo["optimize"] = im.encoderinfo.get("optimize", True)
|
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)
|
header, usedPaletteColors = getheader(imOut, palette, im.encoderinfo)
|
||||||
for s in header:
|
for s in header:
|
||||||
|
@ -314,7 +343,7 @@ def _save(im, fp, filename):
|
||||||
o8(8)) # bits
|
o8(8)) # bits
|
||||||
|
|
||||||
imOut.encoderconfig = (8, interlace)
|
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
|
fp.write(b"\0") # end of image data
|
||||||
|
|
||||||
|
|
|
@ -24,6 +24,7 @@ from PIL._binary import o8
|
||||||
|
|
||||||
EPSILON = 1e-10
|
EPSILON = 1e-10
|
||||||
|
|
||||||
|
|
||||||
def linear(middle, pos):
|
def linear(middle, pos):
|
||||||
if pos <= middle:
|
if pos <= middle:
|
||||||
if middle < EPSILON:
|
if middle < EPSILON:
|
||||||
|
@ -38,25 +39,30 @@ def linear(middle, pos):
|
||||||
else:
|
else:
|
||||||
return 0.5 + 0.5 * pos / middle
|
return 0.5 + 0.5 * pos / middle
|
||||||
|
|
||||||
|
|
||||||
def curved(middle, pos):
|
def curved(middle, pos):
|
||||||
return pos ** (log(0.5) / log(max(middle, EPSILON)))
|
return pos ** (log(0.5) / log(max(middle, EPSILON)))
|
||||||
|
|
||||||
|
|
||||||
def sine(middle, pos):
|
def sine(middle, pos):
|
||||||
return (sin((-pi / 2.0) + pi * linear(middle, pos)) + 1.0) / 2.0
|
return (sin((-pi / 2.0) + pi * linear(middle, pos)) + 1.0) / 2.0
|
||||||
|
|
||||||
|
|
||||||
def sphere_increasing(middle, pos):
|
def sphere_increasing(middle, pos):
|
||||||
return sqrt(1.0 - (linear(middle, pos) - 1.0) ** 2)
|
return sqrt(1.0 - (linear(middle, pos) - 1.0) ** 2)
|
||||||
|
|
||||||
|
|
||||||
def sphere_decreasing(middle, pos):
|
def sphere_decreasing(middle, pos):
|
||||||
return 1.0 - sqrt(1.0 - linear(middle, pos) ** 2)
|
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:
|
class GradientFile:
|
||||||
|
|
||||||
gradient = None
|
gradient = None
|
||||||
|
|
||||||
def getpalette(self, entries = 256):
|
def getpalette(self, entries=256):
|
||||||
|
|
||||||
palette = []
|
palette = []
|
||||||
|
|
||||||
|
@ -89,6 +95,7 @@ class GradientFile:
|
||||||
|
|
||||||
return b"".join(palette), "RGBA"
|
return b"".join(palette), "RGBA"
|
||||||
|
|
||||||
|
|
||||||
##
|
##
|
||||||
# File handler for GIMP's gradient format.
|
# File handler for GIMP's gradient format.
|
||||||
|
|
||||||
|
@ -99,7 +106,13 @@ class GimpGradientFile(GradientFile):
|
||||||
if fp.readline()[:13] != b"GIMP Gradient":
|
if fp.readline()[:13] != b"GIMP Gradient":
|
||||||
raise SyntaxError("not a GIMP gradient file")
|
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 = []
|
gradient = []
|
||||||
|
|
||||||
|
@ -108,13 +121,13 @@ class GimpGradientFile(GradientFile):
|
||||||
s = fp.readline().split()
|
s = fp.readline().split()
|
||||||
w = [float(x) for x in s[:11]]
|
w = [float(x) for x in s[:11]]
|
||||||
|
|
||||||
x0, x1 = w[0], w[2]
|
x0, x1 = w[0], w[2]
|
||||||
xm = w[1]
|
xm = w[1]
|
||||||
rgb0 = w[3:7]
|
rgb0 = w[3:7]
|
||||||
rgb1 = w[7:11]
|
rgb1 = w[7:11]
|
||||||
|
|
||||||
segment = SEGMENTS[int(s[11])]
|
segment = SEGMENTS[int(s[11])]
|
||||||
cspace = int(s[12])
|
cspace = int(s[12])
|
||||||
|
|
||||||
if cspace != 0:
|
if cspace != 0:
|
||||||
raise IOError("cannot handle HSV colour space")
|
raise IOError("cannot handle HSV colour space")
|
||||||
|
|
|
@ -179,6 +179,8 @@ class IcnsFile:
|
||||||
i = HEADERSIZE
|
i = HEADERSIZE
|
||||||
while i < filesize:
|
while i < filesize:
|
||||||
sig, blocksize = nextheader(fobj)
|
sig, blocksize = nextheader(fobj)
|
||||||
|
if blocksize <= 0:
|
||||||
|
raise SyntaxError('invalid block header')
|
||||||
i += HEADERSIZE
|
i += HEADERSIZE
|
||||||
blocksize -= HEADERSIZE
|
blocksize -= HEADERSIZE
|
||||||
dct[sig] = (i, blocksize)
|
dct[sig] = (i, blocksize)
|
||||||
|
|
46
PIL/Image.py
|
@ -220,6 +220,7 @@ _MODEINFO = {
|
||||||
"CMYK": ("RGB", "L", ("C", "M", "Y", "K")),
|
"CMYK": ("RGB", "L", ("C", "M", "Y", "K")),
|
||||||
"YCbCr": ("RGB", "L", ("Y", "Cb", "Cr")),
|
"YCbCr": ("RGB", "L", ("Y", "Cb", "Cr")),
|
||||||
"LAB": ("RGB", "L", ("L", "A", "B")),
|
"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
|
# 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
|
# BGR;24. Use these modes only if you know exactly what you're
|
||||||
|
@ -554,7 +555,6 @@ class Image:
|
||||||
self.readonly = 0
|
self.readonly = 0
|
||||||
|
|
||||||
def _dump(self, file=None, format=None):
|
def _dump(self, file=None, format=None):
|
||||||
import os
|
|
||||||
import tempfile
|
import tempfile
|
||||||
suffix = ''
|
suffix = ''
|
||||||
if format:
|
if format:
|
||||||
|
@ -573,6 +573,8 @@ class Image:
|
||||||
return file
|
return file
|
||||||
|
|
||||||
def __eq__(self, other):
|
def __eq__(self, other):
|
||||||
|
if self.__class__.__name__ != other.__class__.__name__:
|
||||||
|
return False
|
||||||
a = (self.mode == other.mode)
|
a = (self.mode == other.mode)
|
||||||
b = (self.size == other.size)
|
b = (self.size == other.size)
|
||||||
c = (self.getpalette() == other.getpalette())
|
c = (self.getpalette() == other.getpalette())
|
||||||
|
@ -1512,6 +1514,9 @@ class Image:
|
||||||
|
|
||||||
self.load()
|
self.load()
|
||||||
|
|
||||||
|
if self.size == size:
|
||||||
|
return self._new(self.im)
|
||||||
|
|
||||||
if self.mode in ("1", "P"):
|
if self.mode in ("1", "P"):
|
||||||
resample = NEAREST
|
resample = NEAREST
|
||||||
|
|
||||||
|
@ -1908,6 +1913,16 @@ class Image:
|
||||||
im = self.im.transpose(method)
|
im = self.im.transpose(method)
|
||||||
return self._new(im)
|
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
|
# Lazy operations
|
||||||
|
@ -2417,3 +2432,32 @@ def _show(image, **options):
|
||||||
def _showxv(image, title=None, **options):
|
def _showxv(image, title=None, **options):
|
||||||
from PIL import ImageShow
|
from PIL import ImageShow
|
||||||
ImageShow.show(image, title, **options)
|
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
|
||||||
|
|
|
@ -1,19 +1,19 @@
|
||||||
"""
|
## The Python Imaging Library.
|
||||||
The Python Imaging Library.
|
## $Id$
|
||||||
$Id$
|
|
||||||
|
|
||||||
Optional color managment support, based on Kevin Cazabon's PyCMS
|
## Optional color managment support, based on Kevin Cazabon's PyCMS
|
||||||
library.
|
## library.
|
||||||
|
|
||||||
History:
|
## History:
|
||||||
2009-03-08 fl Added to PIL.
|
|
||||||
|
|
||||||
Copyright (C) 2002-2003 Kevin Cazabon
|
## 2009-03-08 fl Added to PIL.
|
||||||
Copyright (c) 2009 by Fredrik Lundh
|
|
||||||
|
|
||||||
See the README file for information on usage and redistribution. See
|
## Copyright (C) 2002-2003 Kevin Cazabon
|
||||||
below for the original description.
|
## 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
|
from __future__ import print_function
|
||||||
|
|
||||||
|
@ -150,8 +150,13 @@ for flag in FLAGS.values():
|
||||||
class ImageCmsProfile:
|
class ImageCmsProfile:
|
||||||
|
|
||||||
def __init__(self, profile):
|
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):
|
if isStringType(profile):
|
||||||
self._set(core.profile_open(profile), profile)
|
self._set(core.profile_open(profile), profile)
|
||||||
elif hasattr(profile, "read"):
|
elif hasattr(profile, "read"):
|
||||||
|
@ -169,12 +174,23 @@ class ImageCmsProfile:
|
||||||
self.product_name = None
|
self.product_name = None
|
||||||
self.product_info = 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):
|
class ImageCmsTransform(Image.ImagePointHandler):
|
||||||
|
|
||||||
"""Transform. This can be used with the procedural API, or with the
|
# Transform. This can be used with the procedural API, or with the
|
||||||
standard Image.point() method.
|
# standard Image.point() method.
|
||||||
"""
|
#
|
||||||
|
# Will return the output profile in the output.info['icc_profile'].
|
||||||
|
|
||||||
|
|
||||||
def __init__(self, input, output, input_mode, output_mode,
|
def __init__(self, input, output, input_mode, output_mode,
|
||||||
intent=INTENT_PERCEPTUAL, proof=None,
|
intent=INTENT_PERCEPTUAL, proof=None,
|
||||||
|
@ -197,6 +213,8 @@ class ImageCmsTransform(Image.ImagePointHandler):
|
||||||
self.input_mode = self.inputMode = input_mode
|
self.input_mode = self.inputMode = input_mode
|
||||||
self.output_mode = self.outputMode = output_mode
|
self.output_mode = self.outputMode = output_mode
|
||||||
|
|
||||||
|
self.output_profile = output
|
||||||
|
|
||||||
def point(self, im):
|
def point(self, im):
|
||||||
return self.apply(im)
|
return self.apply(im)
|
||||||
|
|
||||||
|
@ -205,6 +223,7 @@ class ImageCmsTransform(Image.ImagePointHandler):
|
||||||
if imOut is None:
|
if imOut is None:
|
||||||
imOut = Image.new(self.output_mode, im.size, None)
|
imOut = Image.new(self.output_mode, im.size, None)
|
||||||
self.transform.apply(im.im.id, imOut.im.id)
|
self.transform.apply(im.im.id, imOut.im.id)
|
||||||
|
imOut.info['icc_profile'] = self.output_profile.tobytes()
|
||||||
return imOut
|
return imOut
|
||||||
|
|
||||||
def apply_in_place(self, im):
|
def apply_in_place(self, im):
|
||||||
|
@ -212,6 +231,7 @@ class ImageCmsTransform(Image.ImagePointHandler):
|
||||||
if im.mode != self.output_mode:
|
if im.mode != self.output_mode:
|
||||||
raise ValueError("mode mismatch") # wrong output mode
|
raise ValueError("mode mismatch") # wrong output mode
|
||||||
self.transform.apply(im.im.id, im.im.id)
|
self.transform.apply(im.im.id, im.im.id)
|
||||||
|
im.info['icc_profile'] = self.output_profile.tobytes()
|
||||||
return im
|
return im
|
||||||
|
|
||||||
|
|
||||||
|
@ -570,7 +590,7 @@ def applyTransform(im, transform, inPlace=0):
|
||||||
with the transform applied is returned (and im is not changed). The
|
with the transform applied is returned (and im is not changed). The
|
||||||
default is False.
|
default is False.
|
||||||
:returns: Either None, or a new PIL Image object, depending on the value of
|
: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:
|
:exception PyCMSError:
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@ -637,7 +657,7 @@ def getProfileName(profile):
|
||||||
|
|
||||||
(pyCMS) Gets the internal product name for the given 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
|
a PyCMSError is raised If an error occurs while trying to obtain the
|
||||||
name tag, a PyCMSError is raised.
|
name tag, a PyCMSError is raised.
|
||||||
|
|
||||||
|
@ -876,7 +896,7 @@ def isIntentSupported(profile, intent, direction):
|
||||||
input/output/proof profile as you desire.
|
input/output/proof profile as you desire.
|
||||||
|
|
||||||
Some profiles are created specifically for one "direction", can cannot
|
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
|
rendering intents... so it's best to either verify this before trying
|
||||||
to create a transform with them (using this function), or catch the
|
to create a transform with them (using this function), or catch the
|
||||||
potential PyCMSError that will occur if they don't support the modes
|
potential PyCMSError that will occur if they don't support the modes
|
||||||
|
|
|
@ -133,11 +133,27 @@ class ImageFile(Image.Image):
|
||||||
return pixel
|
return pixel
|
||||||
|
|
||||||
self.map = None
|
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
|
readonly = 0
|
||||||
|
|
||||||
if self.filename and len(self.tile) == 1 and not hasattr(sys, 'pypy_version_info'):
|
# look for read/seek overrides
|
||||||
# As of pypy 2.1.0, memory mapping was failing here.
|
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
|
# try memory mapping
|
||||||
d, e, o, a = self.tile[0]
|
d, e, o, a = self.tile[0]
|
||||||
if d == "raw" and a[0] == self.mode and a[0] in Image._MAPMODES:
|
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()
|
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:
|
if not self.map:
|
||||||
|
|
||||||
# sort tiles in file order
|
# sort tiles in file order
|
||||||
self.tile.sort(key=_tilesort)
|
self.tile.sort(key=_tilesort)
|
||||||
|
|
||||||
|
@ -223,6 +227,8 @@ class ImageFile(Image.Image):
|
||||||
break
|
break
|
||||||
b = b[n:]
|
b = b[n:]
|
||||||
t = t + n
|
t = t + n
|
||||||
|
# Need to cleanup here to prevent leaks in PyPy
|
||||||
|
d.cleanup()
|
||||||
|
|
||||||
self.tile = []
|
self.tile = []
|
||||||
self.readonly = readonly
|
self.readonly = readonly
|
||||||
|
@ -467,6 +473,7 @@ def _save(im, fp, tile, bufsize=0):
|
||||||
break
|
break
|
||||||
if s < 0:
|
if s < 0:
|
||||||
raise IOError("encoder error %d when writing image file" % s)
|
raise IOError("encoder error %d when writing image file" % s)
|
||||||
|
e.cleanup()
|
||||||
else:
|
else:
|
||||||
# slight speedup: compress to real file object
|
# slight speedup: compress to real file object
|
||||||
for e, b, o, a in tile:
|
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)
|
s = e.encode_to_file(fh, bufsize)
|
||||||
if s < 0:
|
if s < 0:
|
||||||
raise IOError("encoder error %d when writing image file" % s)
|
raise IOError("encoder error %d when writing image file" % s)
|
||||||
|
e.cleanup()
|
||||||
try:
|
try:
|
||||||
fp.flush()
|
fp.flush()
|
||||||
except: pass
|
except: pass
|
||||||
|
|
|
@ -29,13 +29,15 @@ from __future__ import print_function
|
||||||
|
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
from PIL._util import isDirectory, isPath
|
from PIL._util import isDirectory, isPath
|
||||||
import os, sys
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import warnings
|
import warnings
|
||||||
except ImportError:
|
except ImportError:
|
||||||
warnings = None
|
warnings = None
|
||||||
|
|
||||||
|
|
||||||
class _imagingft_not_installed:
|
class _imagingft_not_installed:
|
||||||
# module placeholder
|
# module placeholder
|
||||||
def __getattr__(self, id):
|
def __getattr__(self, id):
|
||||||
|
@ -90,8 +92,8 @@ class ImageFont:
|
||||||
# read PILfont header
|
# read PILfont header
|
||||||
if file.readline() != b"PILfont\n":
|
if file.readline() != b"PILfont\n":
|
||||||
raise SyntaxError("Not a PILfont file")
|
raise SyntaxError("Not a PILfont file")
|
||||||
d = file.readline().split(b";")
|
file.readline().split(b";")
|
||||||
self.info = [] # FIXME: should be a dictionary
|
self.info = [] # FIXME: should be a dictionary
|
||||||
while True:
|
while True:
|
||||||
s = file.readline()
|
s = file.readline()
|
||||||
if not s or s == b"DATA\n":
|
if not s or s == b"DATA\n":
|
||||||
|
@ -113,6 +115,7 @@ class ImageFont:
|
||||||
self.getsize = self.font.getsize
|
self.getsize = self.font.getsize
|
||||||
self.getmask = self.font.getmask
|
self.getmask = self.font.getmask
|
||||||
|
|
||||||
|
|
||||||
##
|
##
|
||||||
# Wrapper for FreeType fonts. Application code should use the
|
# Wrapper for FreeType fonts. Application code should use the
|
||||||
# <b>truetype</b> factory function to create font objects.
|
# <b>truetype</b> factory function to create font objects.
|
||||||
|
@ -124,14 +127,18 @@ class FreeTypeFont:
|
||||||
# FIXME: use service provider instead
|
# FIXME: use service provider instead
|
||||||
if file:
|
if file:
|
||||||
if warnings:
|
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
|
font = file
|
||||||
|
|
||||||
if isPath(font):
|
if isPath(font):
|
||||||
self.font = core.getfont(font, size, index, encoding)
|
self.font = core.getfont(font, size, index, encoding)
|
||||||
else:
|
else:
|
||||||
self.font_bytes = font.read()
|
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):
|
def getname(self):
|
||||||
return self.font.family, self.font.style
|
return self.font.family, self.font.style
|
||||||
|
@ -140,7 +147,8 @@ class FreeTypeFont:
|
||||||
return self.font.ascent, self.font.descent
|
return self.font.ascent, self.font.descent
|
||||||
|
|
||||||
def getsize(self, text):
|
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):
|
def getoffset(self, text):
|
||||||
return self.font.getsize(text)[1]
|
return self.font.getsize(text)[1]
|
||||||
|
@ -151,7 +159,7 @@ class FreeTypeFont:
|
||||||
def getmask2(self, text, mode="", fill=Image.core.fill):
|
def getmask2(self, text, mode="", fill=Image.core.fill):
|
||||||
size, offset = self.font.getsize(text)
|
size, offset = self.font.getsize(text)
|
||||||
im = fill("L", size, 0)
|
im = fill("L", size, 0)
|
||||||
self.font.render(text, im.id, mode=="1")
|
self.font.render(text, im.id, mode == "1")
|
||||||
return im, offset
|
return im, offset
|
||||||
|
|
||||||
##
|
##
|
||||||
|
@ -163,12 +171,13 @@ class FreeTypeFont:
|
||||||
# be one of Image.FLIP_LEFT_RIGHT, Image.FLIP_TOP_BOTTOM,
|
# be one of Image.FLIP_LEFT_RIGHT, Image.FLIP_TOP_BOTTOM,
|
||||||
# Image.ROTATE_90, Image.ROTATE_180, or Image.ROTATE_270.
|
# Image.ROTATE_90, Image.ROTATE_180, or Image.ROTATE_270.
|
||||||
|
|
||||||
|
|
||||||
class TransposedFont:
|
class TransposedFont:
|
||||||
"Wrapper for writing rotated or mirrored text"
|
"Wrapper for writing rotated or mirrored text"
|
||||||
|
|
||||||
def __init__(self, font, orientation=None):
|
def __init__(self, font, orientation=None):
|
||||||
self.font = font
|
self.font = font
|
||||||
self.orientation = orientation # any 'transpose' argument, or None
|
self.orientation = orientation # any 'transpose' argument, or None
|
||||||
|
|
||||||
def getsize(self, text):
|
def getsize(self, text):
|
||||||
w, h = self.font.getsize(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 filename:
|
||||||
if warnings:
|
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
|
font = filename
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -272,8 +284,8 @@ def load_default():
|
||||||
import base64
|
import base64
|
||||||
f = ImageFont()
|
f = ImageFont()
|
||||||
f._load_pilfont_data(
|
f._load_pilfont_data(
|
||||||
# courB08
|
# courB08
|
||||||
BytesIO(base64.decodestring(b'''
|
BytesIO(base64.decodestring(b'''
|
||||||
UElMZm9udAo7Ozs7OzsxMDsKREFUQQoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
UElMZm9udAo7Ozs7OzsxMDsKREFUQQoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||||
|
@ -392,15 +404,4 @@ w7IkEbzhVQAAAABJRU5ErkJggg==
|
||||||
'''))))
|
'''))))
|
||||||
return f
|
return f
|
||||||
|
|
||||||
|
# End of file
|
||||||
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("'''))))")
|
|
||||||
|
|
|
@ -26,9 +26,11 @@ except ImportError:
|
||||||
|
|
||||||
VERBOSE = 0
|
VERBOSE = 0
|
||||||
|
|
||||||
|
|
||||||
def _isconstant(v):
|
def _isconstant(v):
|
||||||
return isinstance(v, int) or isinstance(v, float)
|
return isinstance(v, int) or isinstance(v, float)
|
||||||
|
|
||||||
|
|
||||||
class _Operand:
|
class _Operand:
|
||||||
# wraps an image operand, providing standard operators
|
# wraps an image operand, providing standard operators
|
||||||
|
|
||||||
|
@ -68,20 +70,25 @@ class _Operand:
|
||||||
im2 = self.__fixup(im2)
|
im2 = self.__fixup(im2)
|
||||||
if im1.mode != im2.mode:
|
if im1.mode != im2.mode:
|
||||||
# convert both arguments to floating point
|
# convert both arguments to floating point
|
||||||
if im1.mode != "F": im1 = im1.convert("F")
|
if im1.mode != "F":
|
||||||
if im2.mode != "F": im2 = im2.convert("F")
|
im1 = im1.convert("F")
|
||||||
|
if im2.mode != "F":
|
||||||
|
im2 = im2.convert("F")
|
||||||
if im1.mode != im2.mode:
|
if im1.mode != im2.mode:
|
||||||
raise ValueError("mode mismatch")
|
raise ValueError("mode mismatch")
|
||||||
if im1.size != im2.size:
|
if im1.size != im2.size:
|
||||||
# crop both arguments to a common size
|
# crop both arguments to a common size
|
||||||
size = (min(im1.size[0], im2.size[0]),
|
size = (min(im1.size[0], im2.size[0]),
|
||||||
min(im1.size[1], im2.size[1]))
|
min(im1.size[1], im2.size[1]))
|
||||||
if im1.size != size: im1 = im1.crop((0, 0) + size)
|
if im1.size != size:
|
||||||
if im2.size != size: im2 = im2.crop((0, 0) + 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)
|
out = Image.new(mode or im1.mode, size, None)
|
||||||
else:
|
else:
|
||||||
out = Image.new(mode or im1.mode, im1.size, None)
|
out = Image.new(mode or im1.mode, im1.size, None)
|
||||||
im1.load(); im2.load()
|
im1.load()
|
||||||
|
im2.load()
|
||||||
try:
|
try:
|
||||||
op = getattr(_imagingmath, op+"_"+im1.mode)
|
op = getattr(_imagingmath, op+"_"+im1.mode)
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
|
@ -101,34 +108,47 @@ class _Operand:
|
||||||
|
|
||||||
def __abs__(self):
|
def __abs__(self):
|
||||||
return self.apply("abs", self)
|
return self.apply("abs", self)
|
||||||
|
|
||||||
def __pos__(self):
|
def __pos__(self):
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def __neg__(self):
|
def __neg__(self):
|
||||||
return self.apply("neg", self)
|
return self.apply("neg", self)
|
||||||
|
|
||||||
# binary operators
|
# binary operators
|
||||||
def __add__(self, other):
|
def __add__(self, other):
|
||||||
return self.apply("add", self, other)
|
return self.apply("add", self, other)
|
||||||
|
|
||||||
def __radd__(self, other):
|
def __radd__(self, other):
|
||||||
return self.apply("add", other, self)
|
return self.apply("add", other, self)
|
||||||
|
|
||||||
def __sub__(self, other):
|
def __sub__(self, other):
|
||||||
return self.apply("sub", self, other)
|
return self.apply("sub", self, other)
|
||||||
|
|
||||||
def __rsub__(self, other):
|
def __rsub__(self, other):
|
||||||
return self.apply("sub", other, self)
|
return self.apply("sub", other, self)
|
||||||
|
|
||||||
def __mul__(self, other):
|
def __mul__(self, other):
|
||||||
return self.apply("mul", self, other)
|
return self.apply("mul", self, other)
|
||||||
|
|
||||||
def __rmul__(self, other):
|
def __rmul__(self, other):
|
||||||
return self.apply("mul", other, self)
|
return self.apply("mul", other, self)
|
||||||
|
|
||||||
def __truediv__(self, other):
|
def __truediv__(self, other):
|
||||||
return self.apply("div", self, other)
|
return self.apply("div", self, other)
|
||||||
|
|
||||||
def __rtruediv__(self, other):
|
def __rtruediv__(self, other):
|
||||||
return self.apply("div", other, self)
|
return self.apply("div", other, self)
|
||||||
|
|
||||||
def __mod__(self, other):
|
def __mod__(self, other):
|
||||||
return self.apply("mod", self, other)
|
return self.apply("mod", self, other)
|
||||||
|
|
||||||
def __rmod__(self, other):
|
def __rmod__(self, other):
|
||||||
return self.apply("mod", other, self)
|
return self.apply("mod", other, self)
|
||||||
|
|
||||||
def __pow__(self, other):
|
def __pow__(self, other):
|
||||||
return self.apply("pow", self, other)
|
return self.apply("pow", self, other)
|
||||||
|
|
||||||
def __rpow__(self, other):
|
def __rpow__(self, other):
|
||||||
return self.apply("pow", other, self)
|
return self.apply("pow", other, self)
|
||||||
|
|
||||||
|
@ -142,54 +162,77 @@ class _Operand:
|
||||||
# bitwise
|
# bitwise
|
||||||
def __invert__(self):
|
def __invert__(self):
|
||||||
return self.apply("invert", self)
|
return self.apply("invert", self)
|
||||||
|
|
||||||
def __and__(self, other):
|
def __and__(self, other):
|
||||||
return self.apply("and", self, other)
|
return self.apply("and", self, other)
|
||||||
|
|
||||||
def __rand__(self, other):
|
def __rand__(self, other):
|
||||||
return self.apply("and", other, self)
|
return self.apply("and", other, self)
|
||||||
|
|
||||||
def __or__(self, other):
|
def __or__(self, other):
|
||||||
return self.apply("or", self, other)
|
return self.apply("or", self, other)
|
||||||
|
|
||||||
def __ror__(self, other):
|
def __ror__(self, other):
|
||||||
return self.apply("or", other, self)
|
return self.apply("or", other, self)
|
||||||
|
|
||||||
def __xor__(self, other):
|
def __xor__(self, other):
|
||||||
return self.apply("xor", self, other)
|
return self.apply("xor", self, other)
|
||||||
|
|
||||||
def __rxor__(self, other):
|
def __rxor__(self, other):
|
||||||
return self.apply("xor", other, self)
|
return self.apply("xor", other, self)
|
||||||
|
|
||||||
def __lshift__(self, other):
|
def __lshift__(self, other):
|
||||||
return self.apply("lshift", self, other)
|
return self.apply("lshift", self, other)
|
||||||
|
|
||||||
def __rshift__(self, other):
|
def __rshift__(self, other):
|
||||||
return self.apply("rshift", self, other)
|
return self.apply("rshift", self, other)
|
||||||
|
|
||||||
# logical
|
# logical
|
||||||
def __eq__(self, other):
|
def __eq__(self, other):
|
||||||
return self.apply("eq", self, other)
|
return self.apply("eq", self, other)
|
||||||
|
|
||||||
def __ne__(self, other):
|
def __ne__(self, other):
|
||||||
return self.apply("ne", self, other)
|
return self.apply("ne", self, other)
|
||||||
|
|
||||||
def __lt__(self, other):
|
def __lt__(self, other):
|
||||||
return self.apply("lt", self, other)
|
return self.apply("lt", self, other)
|
||||||
|
|
||||||
def __le__(self, other):
|
def __le__(self, other):
|
||||||
return self.apply("le", self, other)
|
return self.apply("le", self, other)
|
||||||
|
|
||||||
def __gt__(self, other):
|
def __gt__(self, other):
|
||||||
return self.apply("gt", self, other)
|
return self.apply("gt", self, other)
|
||||||
|
|
||||||
def __ge__(self, other):
|
def __ge__(self, other):
|
||||||
return self.apply("ge", self, other)
|
return self.apply("ge", self, other)
|
||||||
|
|
||||||
|
|
||||||
# conversions
|
# conversions
|
||||||
def imagemath_int(self):
|
def imagemath_int(self):
|
||||||
return _Operand(self.im.convert("I"))
|
return _Operand(self.im.convert("I"))
|
||||||
|
|
||||||
|
|
||||||
def imagemath_float(self):
|
def imagemath_float(self):
|
||||||
return _Operand(self.im.convert("F"))
|
return _Operand(self.im.convert("F"))
|
||||||
|
|
||||||
|
|
||||||
# logical
|
# logical
|
||||||
def imagemath_equal(self, other):
|
def imagemath_equal(self, other):
|
||||||
return self.apply("eq", self, other, mode="I")
|
return self.apply("eq", self, other, mode="I")
|
||||||
|
|
||||||
|
|
||||||
def imagemath_notequal(self, other):
|
def imagemath_notequal(self, other):
|
||||||
return self.apply("ne", self, other, mode="I")
|
return self.apply("ne", self, other, mode="I")
|
||||||
|
|
||||||
|
|
||||||
def imagemath_min(self, other):
|
def imagemath_min(self, other):
|
||||||
return self.apply("min", self, other)
|
return self.apply("min", self, other)
|
||||||
|
|
||||||
|
|
||||||
def imagemath_max(self, other):
|
def imagemath_max(self, other):
|
||||||
return self.apply("max", self, other)
|
return self.apply("max", self, other)
|
||||||
|
|
||||||
|
|
||||||
def imagemath_convert(self, mode):
|
def imagemath_convert(self, mode):
|
||||||
return _Operand(self.im.convert(mode))
|
return _Operand(self.im.convert(mode))
|
||||||
|
|
||||||
|
|
|
@ -15,19 +15,19 @@ LUT_SIZE = 1 << 9
|
||||||
class LutBuilder:
|
class LutBuilder:
|
||||||
"""A class for building a MorphLut from a descriptive language
|
"""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:(...
|
4:(...
|
||||||
.1.
|
.1.
|
||||||
111)->1
|
111)->1
|
||||||
|
|
||||||
(whitespaces including linebreaks are ignored). The option 4
|
(whitespaces including linebreaks are ignored). The option 4
|
||||||
describes a series of symmetry operations (in this case a
|
describes a series of symmetry operations (in this case a
|
||||||
4-rotation), the pattern is described by:
|
4-rotation), the pattern is described by:
|
||||||
|
|
||||||
. or X - Ignore
|
- . or X - Ignore
|
||||||
1 - Pixel is on
|
- 1 - Pixel is on
|
||||||
0 - Pixel is off
|
- 0 - Pixel is off
|
||||||
|
|
||||||
The result of the operation is described after "->" string.
|
The result of the operation is described after "->" string.
|
||||||
|
|
||||||
|
@ -35,15 +35,16 @@ class LutBuilder:
|
||||||
returned if no other match is found.
|
returned if no other match is found.
|
||||||
|
|
||||||
Operations:
|
Operations:
|
||||||
4 - 4 way rotation
|
|
||||||
N - Negate
|
- 4 - 4 way rotation
|
||||||
1 - Dummy op for no other operation (an op must always be given)
|
- N - Negate
|
||||||
M - Mirroring
|
- 1 - Dummy op for no other operation (an op must always be given)
|
||||||
|
- M - Mirroring
|
||||||
|
|
||||||
Example:
|
Example::
|
||||||
|
|
||||||
lb = LutBuilder(patterns = ["4:(... .1. 111)->1"])
|
lb = LutBuilder(patterns = ["4:(... .1. 111)->1"])
|
||||||
lut = lb.build_lut()
|
lut = lb.build_lut()
|
||||||
|
|
||||||
"""
|
"""
|
||||||
def __init__(self, patterns=None, op_name=None):
|
def __init__(self, patterns=None, op_name=None):
|
||||||
|
|
|
@ -392,7 +392,7 @@ def solarize(image, threshold=128):
|
||||||
"""
|
"""
|
||||||
Invert all pixel values above a threshold.
|
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.
|
:param threshold: All pixels above this greyscale level are inverted.
|
||||||
:return: An image.
|
:return: An image.
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -17,19 +17,20 @@
|
||||||
#
|
#
|
||||||
|
|
||||||
import array
|
import array
|
||||||
from PIL import Image, ImageColor
|
import warnings
|
||||||
|
from PIL import ImageColor
|
||||||
|
|
||||||
|
|
||||||
class ImagePalette:
|
class ImagePalette:
|
||||||
"Color palette for palette mapped images"
|
"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.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.palette = palette or list(range(256))*len(self.mode)
|
||||||
self.colors = {}
|
self.colors = {}
|
||||||
self.dirty = None
|
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))):
|
(size != 0 and size != len(self.palette))):
|
||||||
raise ValueError("wrong palette size")
|
raise ValueError("wrong palette size")
|
||||||
|
|
||||||
|
@ -55,7 +56,7 @@ class ImagePalette:
|
||||||
return self.palette
|
return self.palette
|
||||||
arr = array.array("B", self.palette)
|
arr = array.array("B", self.palette)
|
||||||
if hasattr(arr, 'tobytes'):
|
if hasattr(arr, 'tobytes'):
|
||||||
#py3k has a tobytes, tostring is deprecated.
|
# py3k has a tobytes, tostring is deprecated.
|
||||||
return arr.tobytes()
|
return arr.tobytes()
|
||||||
return arr.tostring()
|
return arr.tostring()
|
||||||
|
|
||||||
|
@ -109,6 +110,7 @@ class ImagePalette:
|
||||||
fp.write("\n")
|
fp.write("\n")
|
||||||
fp.close()
|
fp.close()
|
||||||
|
|
||||||
|
|
||||||
# --------------------------------------------------------------------
|
# --------------------------------------------------------------------
|
||||||
# Internal
|
# Internal
|
||||||
|
|
||||||
|
@ -119,32 +121,53 @@ def raw(rawmode, data):
|
||||||
palette.dirty = 1
|
palette.dirty = 1
|
||||||
return palette
|
return palette
|
||||||
|
|
||||||
|
|
||||||
# --------------------------------------------------------------------
|
# --------------------------------------------------------------------
|
||||||
# Factories
|
# Factories
|
||||||
|
|
||||||
def _make_linear_lut(black, white):
|
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 = []
|
lut = []
|
||||||
if black == 0:
|
if black == 0:
|
||||||
for i in range(256):
|
for i in range(256):
|
||||||
lut.append(white*i//255)
|
lut.append(white*i//255)
|
||||||
else:
|
else:
|
||||||
raise NotImplementedError # FIXME
|
raise NotImplementedError # FIXME
|
||||||
return lut
|
return lut
|
||||||
|
|
||||||
def _make_gamma_lut(exp, mode="RGB"):
|
|
||||||
|
def make_gamma_lut(exp):
|
||||||
lut = []
|
lut = []
|
||||||
for i in range(256):
|
for i in range(256):
|
||||||
lut.append(int(((i / 255.0) ** exp) * 255.0 + 0.5))
|
lut.append(int(((i / 255.0) ** exp) * 255.0 + 0.5))
|
||||||
return lut
|
return lut
|
||||||
|
|
||||||
def new(mode, data):
|
|
||||||
return Image.core.new_palette(mode, data)
|
|
||||||
|
|
||||||
def negative(mode="RGB"):
|
def negative(mode="RGB"):
|
||||||
palette = list(range(256))
|
palette = list(range(256))
|
||||||
palette.reverse()
|
palette.reverse()
|
||||||
return ImagePalette(mode, palette * len(mode))
|
return ImagePalette(mode, palette * len(mode))
|
||||||
|
|
||||||
|
|
||||||
def random(mode="RGB"):
|
def random(mode="RGB"):
|
||||||
from random import randint
|
from random import randint
|
||||||
palette = []
|
palette = []
|
||||||
|
@ -152,16 +175,19 @@ def random(mode="RGB"):
|
||||||
palette.append(randint(0, 255))
|
palette.append(randint(0, 255))
|
||||||
return ImagePalette(mode, palette)
|
return ImagePalette(mode, palette)
|
||||||
|
|
||||||
|
|
||||||
def sepia(white="#fff0c0"):
|
def sepia(white="#fff0c0"):
|
||||||
r, g, b = ImageColor.getrgb(white)
|
r, g, b = ImageColor.getrgb(white)
|
||||||
r = _make_linear_lut(0, r)
|
r = make_linear_lut(0, r)
|
||||||
g = _make_linear_lut(0, g)
|
g = make_linear_lut(0, g)
|
||||||
b = _make_linear_lut(0, b)
|
b = make_linear_lut(0, b)
|
||||||
return ImagePalette("RGB", r + g + b)
|
return ImagePalette("RGB", r + g + b)
|
||||||
|
|
||||||
|
|
||||||
def wedge(mode="RGB"):
|
def wedge(mode="RGB"):
|
||||||
return ImagePalette(mode, list(range(256)) * len(mode))
|
return ImagePalette(mode, list(range(256)) * len(mode))
|
||||||
|
|
||||||
|
|
||||||
def load(filename):
|
def load(filename):
|
||||||
|
|
||||||
# FIXME: supports GIMP gradients only
|
# FIXME: supports GIMP gradients only
|
||||||
|
@ -177,8 +203,8 @@ def load(filename):
|
||||||
p = GimpPaletteFile.GimpPaletteFile(fp)
|
p = GimpPaletteFile.GimpPaletteFile(fp)
|
||||||
lut = p.getpalette()
|
lut = p.getpalette()
|
||||||
except (SyntaxError, ValueError):
|
except (SyntaxError, ValueError):
|
||||||
#import traceback
|
# import traceback
|
||||||
#traceback.print_exc()
|
# traceback.print_exc()
|
||||||
pass
|
pass
|
||||||
|
|
||||||
if not lut:
|
if not lut:
|
||||||
|
@ -188,8 +214,8 @@ def load(filename):
|
||||||
p = GimpGradientFile.GimpGradientFile(fp)
|
p = GimpGradientFile.GimpGradientFile(fp)
|
||||||
lut = p.getpalette()
|
lut = p.getpalette()
|
||||||
except (SyntaxError, ValueError):
|
except (SyntaxError, ValueError):
|
||||||
#import traceback
|
# import traceback
|
||||||
#traceback.print_exc()
|
# traceback.print_exc()
|
||||||
pass
|
pass
|
||||||
|
|
||||||
if not lut:
|
if not lut:
|
||||||
|
@ -206,4 +232,4 @@ def load(filename):
|
||||||
if not lut:
|
if not lut:
|
||||||
raise IOError("cannot load palette")
|
raise IOError("cannot load palette")
|
||||||
|
|
||||||
return lut # data, rawmode
|
return lut # data, rawmode
|
||||||
|
|
|
@ -21,7 +21,8 @@ __version__ = "0.3"
|
||||||
|
|
||||||
|
|
||||||
from PIL import Image, ImageFile, _binary
|
from PIL import Image, ImageFile, _binary
|
||||||
import os, tempfile
|
import os
|
||||||
|
import tempfile
|
||||||
|
|
||||||
i8 = _binary.i8
|
i8 = _binary.i8
|
||||||
i16 = _binary.i16be
|
i16 = _binary.i16be
|
||||||
|
@ -35,17 +36,20 @@ COMPRESSION = {
|
||||||
|
|
||||||
PAD = o8(0) * 4
|
PAD = o8(0) * 4
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Helpers
|
# Helpers
|
||||||
|
|
||||||
def i(c):
|
def i(c):
|
||||||
return i32((PAD + c)[-4:])
|
return i32((PAD + c)[-4:])
|
||||||
|
|
||||||
|
|
||||||
def dump(c):
|
def dump(c):
|
||||||
for i in c:
|
for i in c:
|
||||||
print("%02x" % i8(i), end=' ')
|
print("%02x" % i8(i), end=' ')
|
||||||
print()
|
print()
|
||||||
|
|
||||||
|
|
||||||
##
|
##
|
||||||
# Image plugin for IPTC/NAA datastreams. To read IPTC/NAA fields
|
# Image plugin for IPTC/NAA datastreams. To read IPTC/NAA fields
|
||||||
# from TIFF and JPEG files, use the <b>getiptcinfo</b> function.
|
# from TIFF and JPEG files, use the <b>getiptcinfo</b> function.
|
||||||
|
@ -84,35 +88,13 @@ class IptcImageFile(ImageFile.ImageFile):
|
||||||
|
|
||||||
return tag, size
|
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):
|
def _open(self):
|
||||||
|
|
||||||
# load descriptive fields
|
# load descriptive fields
|
||||||
while True:
|
while True:
|
||||||
offset = self.fp.tell()
|
offset = self.fp.tell()
|
||||||
tag, size = self.field()
|
tag, size = self.field()
|
||||||
if not tag or tag == (8,10):
|
if not tag or tag == (8, 10):
|
||||||
break
|
break
|
||||||
if size:
|
if size:
|
||||||
tagdata = self.fp.read(size)
|
tagdata = self.fp.read(size)
|
||||||
|
@ -129,10 +111,10 @@ class IptcImageFile(ImageFile.ImageFile):
|
||||||
# print tag, self.info[tag]
|
# print tag, self.info[tag]
|
||||||
|
|
||||||
# mode
|
# mode
|
||||||
layers = i8(self.info[(3,60)][0])
|
layers = i8(self.info[(3, 60)][0])
|
||||||
component = i8(self.info[(3,60)][1])
|
component = i8(self.info[(3, 60)][1])
|
||||||
if (3,65) in self.info:
|
if (3, 65) in self.info:
|
||||||
id = i8(self.info[(3,65)][0])-1
|
id = i8(self.info[(3, 65)][0])-1
|
||||||
else:
|
else:
|
||||||
id = 0
|
id = 0
|
||||||
if layers == 1 and not component:
|
if layers == 1 and not component:
|
||||||
|
@ -143,22 +125,18 @@ class IptcImageFile(ImageFile.ImageFile):
|
||||||
self.mode = "CMYK"[id]
|
self.mode = "CMYK"[id]
|
||||||
|
|
||||||
# size
|
# size
|
||||||
self.size = self.getint((3,20)), self.getint((3,30))
|
self.size = self.getint((3, 20)), self.getint((3, 30))
|
||||||
|
|
||||||
# compression
|
# compression
|
||||||
try:
|
try:
|
||||||
compression = COMPRESSION[self.getint((3,120))]
|
compression = COMPRESSION[self.getint((3, 120))]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
raise IOError("Unknown IPTC image compression")
|
raise IOError("Unknown IPTC image compression")
|
||||||
|
|
||||||
# tile
|
# tile
|
||||||
if tag == (8,10):
|
if tag == (8, 10):
|
||||||
if compression == "raw" and self._is_raw(offset, self.size):
|
self.tile = [("iptc", (compression, offset),
|
||||||
self.tile = [(compression, (offset, size + 5, -1),
|
(0, 0, self.size[0], self.size[1]))]
|
||||||
(0, 0, self.size[0], self.size[1]))]
|
|
||||||
else:
|
|
||||||
self.tile = [("iptc", (compression, offset),
|
|
||||||
(0, 0, self.size[0], self.size[1]))]
|
|
||||||
|
|
||||||
def load(self):
|
def load(self):
|
||||||
|
|
||||||
|
@ -200,14 +178,17 @@ class IptcImageFile(ImageFile.ImageFile):
|
||||||
im.load()
|
im.load()
|
||||||
self.im = im.im
|
self.im = im.im
|
||||||
finally:
|
finally:
|
||||||
try: os.unlink(outfile)
|
try:
|
||||||
except: pass
|
os.unlink(outfile)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
Image.register_open("IPTC", IptcImageFile)
|
Image.register_open("IPTC", IptcImageFile)
|
||||||
|
|
||||||
Image.register_extension("IPTC", ".iim")
|
Image.register_extension("IPTC", ".iim")
|
||||||
|
|
||||||
|
|
||||||
##
|
##
|
||||||
# Get IPTC information from TIFF, JPEG, or IPTC file.
|
# Get IPTC information from TIFF, JPEG, or IPTC file.
|
||||||
#
|
#
|
||||||
|
@ -230,11 +211,11 @@ def getiptcinfo(im):
|
||||||
# extract the IPTC/NAA resource
|
# extract the IPTC/NAA resource
|
||||||
try:
|
try:
|
||||||
app = im.app["APP13"]
|
app = im.app["APP13"]
|
||||||
if app[:14] == "Photoshop 3.0\x00":
|
if app[:14] == b"Photoshop 3.0\x00":
|
||||||
app = app[14:]
|
app = app[14:]
|
||||||
# parse the image resource block
|
# parse the image resource block
|
||||||
offset = 0
|
offset = 0
|
||||||
while app[offset:offset+4] == "8BIM":
|
while app[offset:offset+4] == b"8BIM":
|
||||||
offset += 4
|
offset += 4
|
||||||
# resource code
|
# resource code
|
||||||
code = JpegImagePlugin.i16(app, offset)
|
code = JpegImagePlugin.i16(app, offset)
|
||||||
|
@ -267,7 +248,7 @@ def getiptcinfo(im):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
if data is None:
|
if data is None:
|
||||||
return None # no properties
|
return None # no properties
|
||||||
|
|
||||||
# create an IptcImagePlugin object without initializing it
|
# create an IptcImagePlugin object without initializing it
|
||||||
class FakeImage:
|
class FakeImage:
|
||||||
|
@ -282,6 +263,6 @@ def getiptcinfo(im):
|
||||||
try:
|
try:
|
||||||
im._open()
|
im._open()
|
||||||
except (IndexError, KeyError):
|
except (IndexError, KeyError):
|
||||||
pass # expected failure
|
pass # expected failure
|
||||||
|
|
||||||
return im.info
|
return im.info
|
||||||
|
|
|
@ -70,6 +70,9 @@ def _parse_jp2_header(fp):
|
||||||
else:
|
else:
|
||||||
hlen = 8
|
hlen = 8
|
||||||
|
|
||||||
|
if lbox < hlen:
|
||||||
|
raise SyntaxError('Invalid JP2 header length')
|
||||||
|
|
||||||
if tbox == b'jp2h':
|
if tbox == b'jp2h':
|
||||||
header = fp.read(lbox - hlen)
|
header = fp.read(lbox - hlen)
|
||||||
break
|
break
|
||||||
|
|
|
@ -36,7 +36,9 @@ __version__ = "0.6"
|
||||||
|
|
||||||
import array
|
import array
|
||||||
import struct
|
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.JpegPresets import presets
|
||||||
from PIL._util import isStringType
|
from PIL._util import isStringType
|
||||||
|
|
||||||
|
@ -110,6 +112,11 @@ def APP(self, marker):
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
self.info["adobe_transform"] = adobe_transform
|
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):
|
def COM(self, marker):
|
||||||
|
@ -380,18 +387,22 @@ class JpegImageFile(ImageFile.ImageFile):
|
||||||
def _getexif(self):
|
def _getexif(self):
|
||||||
return _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):
|
def _getexif(self):
|
||||||
# Extract EXIF information. This method is highly experimental,
|
# Extract EXIF information. This method is highly experimental,
|
||||||
# and is likely to be replaced with something better in a future
|
# and is likely to be replaced with something better in a future
|
||||||
# version.
|
# 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
|
# The EXIF record consists of a TIFF file embedded in a JPEG
|
||||||
# application marker (!).
|
# application marker (!).
|
||||||
try:
|
try:
|
||||||
|
@ -405,7 +416,7 @@ def _getexif(self):
|
||||||
info = TiffImagePlugin.ImageFileDirectory(head)
|
info = TiffImagePlugin.ImageFileDirectory(head)
|
||||||
info.load(file)
|
info.load(file)
|
||||||
for key, value in info.items():
|
for key, value in info.items():
|
||||||
exif[key] = fixup(value)
|
exif[key] = _fixup(value)
|
||||||
# get exif extension
|
# get exif extension
|
||||||
try:
|
try:
|
||||||
file.seek(exif[0x8769])
|
file.seek(exif[0x8769])
|
||||||
|
@ -415,7 +426,7 @@ def _getexif(self):
|
||||||
info = TiffImagePlugin.ImageFileDirectory(head)
|
info = TiffImagePlugin.ImageFileDirectory(head)
|
||||||
info.load(file)
|
info.load(file)
|
||||||
for key, value in info.items():
|
for key, value in info.items():
|
||||||
exif[key] = fixup(value)
|
exif[key] = _fixup(value)
|
||||||
# get gpsinfo extension
|
# get gpsinfo extension
|
||||||
try:
|
try:
|
||||||
file.seek(exif[0x8825])
|
file.seek(exif[0x8825])
|
||||||
|
@ -426,9 +437,77 @@ def _getexif(self):
|
||||||
info.load(file)
|
info.load(file)
|
||||||
exif[0x8825] = gps = {}
|
exif[0x8825] = gps = {}
|
||||||
for key, value in info.items():
|
for key, value in info.items():
|
||||||
gps[key] = fixup(value)
|
gps[key] = _fixup(value)
|
||||||
return exif
|
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
|
# stuff to save JPEG files
|
||||||
|
|
||||||
|
@ -466,6 +545,15 @@ def convert_dict_qtables(qtables):
|
||||||
|
|
||||||
|
|
||||||
def get_sampling(im):
|
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]
|
sampling = im.layer[0][1:3] + im.layer[1][1:3] + im.layer[2][1:3]
|
||||||
return samplings.get(sampling, -1)
|
return samplings.get(sampling, -1)
|
||||||
|
|
||||||
|
@ -611,10 +699,27 @@ def _save_cjpeg(im, fp, filename):
|
||||||
except:
|
except:
|
||||||
pass
|
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-
|
# -------------------------------------------------------------------q-
|
||||||
# Registry stuff
|
# Registry stuff
|
||||||
|
|
||||||
Image.register_open("JPEG", JpegImageFile, _accept)
|
Image.register_open("JPEG", jpeg_factory, _accept)
|
||||||
Image.register_save("JPEG", _save)
|
Image.register_save("JPEG", _save)
|
||||||
|
|
||||||
Image.register_extension("JPEG", ".jfif")
|
Image.register_extension("JPEG", ".jfif")
|
||||||
|
|
87
PIL/MpoImagePlugin.py
Normal file
|
@ -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")
|
197
PIL/OleFileIO.py
|
@ -1,28 +1,29 @@
|
||||||
#!/usr/local/bin/python
|
#!/usr/local/bin/python
|
||||||
# -*- coding: latin-1 -*-
|
# -*- coding: latin-1 -*-
|
||||||
"""
|
## OleFileIO_PL:
|
||||||
OleFileIO_PL:
|
## Module to read Microsoft OLE2 files (also called Structured Storage or
|
||||||
Module to read Microsoft OLE2 files (also called Structured Storage or
|
## Microsoft Compound Document File Format), such as Microsoft Office
|
||||||
Microsoft Compound Document File Format), such as Microsoft Office
|
## documents, Image Composer and FlashPix files, Outlook messages, ...
|
||||||
documents, Image Composer and FlashPix files, Outlook messages, ...
|
## This version is compatible with Python 2.6+ and 3.x
|
||||||
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
|
## Improved version of the OleFileIO module from PIL library v1.1.6
|
||||||
See: http://www.pythonware.com/products/pil/index.htm
|
## See: http://www.pythonware.com/products/pil/index.htm
|
||||||
|
|
||||||
The Python Imaging Library (PIL) is
|
## 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
|
|
||||||
|
|
||||||
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
|
# 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
|
# This import enables print() as a function rather than a keyword
|
||||||
|
@ -370,8 +371,9 @@ for key in list(vars().keys()):
|
||||||
def isOleFile (filename):
|
def isOleFile (filename):
|
||||||
"""
|
"""
|
||||||
Test if file is an OLE container (according to its header).
|
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')
|
f = open(filename, 'rb')
|
||||||
header = f.read(len(MAGIC))
|
header = f.read(len(MAGIC))
|
||||||
|
@ -397,8 +399,8 @@ def i16(c, o = 0):
|
||||||
"""
|
"""
|
||||||
Converts a 2-bytes (16 bits) string to an integer.
|
Converts a 2-bytes (16 bits) string to an integer.
|
||||||
|
|
||||||
c: string containing bytes to convert
|
:param c: string containing bytes to convert
|
||||||
o: offset of bytes to convert in string
|
:param o: offset of bytes to convert in string
|
||||||
"""
|
"""
|
||||||
return i8(c[o]) | (i8(c[o+1])<<8)
|
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.
|
Converts a 4-bytes (32 bits) string to an integer.
|
||||||
|
|
||||||
c: string containing bytes to convert
|
:param c: string containing bytes to convert
|
||||||
o: offset of bytes to convert in string
|
: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))
|
## 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
|
## # [PL]: added int() because "<<" gives long int since Python 2.4
|
||||||
|
@ -419,7 +421,8 @@ def i32(c, o = 0):
|
||||||
def _clsid(clsid):
|
def _clsid(clsid):
|
||||||
"""
|
"""
|
||||||
Converts a CLSID to a human-readable string.
|
Converts a CLSID to a human-readable string.
|
||||||
clsid: string of length 16.
|
|
||||||
|
:param clsid: string of length 16.
|
||||||
"""
|
"""
|
||||||
assert len(clsid) == 16
|
assert len(clsid) == 16
|
||||||
# if clsid is only made of null bytes, return an empty string:
|
# 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)
|
Map unicode string to Latin 1. (Python with Unicode support)
|
||||||
|
|
||||||
s: UTF-16LE unicode string to convert to Latin-1
|
:param s: UTF-16LE unicode string to convert to Latin-1
|
||||||
errors: 'replace', 'ignore' or 'strict'.
|
:param errors: 'replace', 'ignore' or 'strict'.
|
||||||
"""
|
"""
|
||||||
#TODO: test if it OleFileIO works with Unicode strings, instead of
|
#TODO: test if it OleFileIO works with Unicode strings, instead of
|
||||||
# converting to Latin-1.
|
# converting to Latin-1.
|
||||||
|
@ -650,14 +653,14 @@ class _OleStream(io.BytesIO):
|
||||||
"""
|
"""
|
||||||
Constructor for _OleStream class.
|
Constructor for _OleStream class.
|
||||||
|
|
||||||
fp : file object, the OLE container or the MiniFAT stream
|
:param fp : file object, the OLE container or the MiniFAT stream
|
||||||
sect : sector index of first sector in the stream
|
:param sect : sector index of first sector in the stream
|
||||||
size : total size of the stream
|
:param size : total size of the stream
|
||||||
offset : offset in bytes for the first FAT or MiniFAT sector
|
:param offset : offset in bytes for the first FAT or MiniFAT sector
|
||||||
sectorsize: size of one sector
|
:param sectorsize: size of one sector
|
||||||
fat : array/list of sector indexes (FAT or MiniFAT)
|
:param fat : array/list of sector indexes (FAT or MiniFAT)
|
||||||
filesize : size of OLE file (for debugging)
|
:param filesize : size of OLE file (for debugging)
|
||||||
return : a BytesIO instance containing the OLE stream
|
:returns : a BytesIO instance containing the OLE stream
|
||||||
"""
|
"""
|
||||||
debug('_OleStream.__init__:')
|
debug('_OleStream.__init__:')
|
||||||
debug(' sect=%d (%X), size=%d, offset=%d, sectorsize=%d, len(fat)=%d, fp=%s'
|
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.
|
Constructor for an _OleDirectoryEntry object.
|
||||||
Parses a 128-bytes entry from the OLE Directory stream.
|
Parses a 128-bytes entry from the OLE Directory stream.
|
||||||
|
|
||||||
entry : string (must be 128 bytes long)
|
:param entry : string (must be 128 bytes long)
|
||||||
sid : index of this directory entry in the OLE file directory
|
:param sid : index of this directory entry in the OLE file directory
|
||||||
olefile: OleFileIO containing this directory entry
|
:param olefile: OleFileIO containing this directory entry
|
||||||
"""
|
"""
|
||||||
self.sid = sid
|
self.sid = sid
|
||||||
# ref to olefile is stored for future use
|
# ref to olefile is stored for future use
|
||||||
|
@ -989,7 +992,7 @@ class _OleDirectoryEntry:
|
||||||
"""
|
"""
|
||||||
Return modification time of a directory entry.
|
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)
|
otherwise (UTC timezone)
|
||||||
|
|
||||||
new in version 0.26
|
new in version 0.26
|
||||||
|
@ -1003,7 +1006,7 @@ class _OleDirectoryEntry:
|
||||||
"""
|
"""
|
||||||
Return creation time of a directory entry.
|
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)
|
otherwise (UTC timezone)
|
||||||
|
|
||||||
new in version 0.26
|
new in version 0.26
|
||||||
|
@ -1020,7 +1023,8 @@ class OleFileIO:
|
||||||
OLE container object
|
OLE container object
|
||||||
|
|
||||||
This class encapsulates the interface to an OLE 2 structured
|
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.
|
access the contents of this file.
|
||||||
|
|
||||||
Object names are given as a list of strings, one for each subentry
|
Object names are given as a list of strings, one for each subentry
|
||||||
|
@ -1048,8 +1052,8 @@ class OleFileIO:
|
||||||
"""
|
"""
|
||||||
Constructor for OleFileIO class.
|
Constructor for OleFileIO class.
|
||||||
|
|
||||||
filename: file to open.
|
:param filename: file to open.
|
||||||
raise_defects: minimal level for defects to be raised as exceptions.
|
:param raise_defects: minimal level for defects to be raised as exceptions.
|
||||||
(use DEFECT_FATAL for a typical application, DEFECT_INCORRECT for a
|
(use DEFECT_FATAL for a typical application, DEFECT_INCORRECT for a
|
||||||
security-oriented application, see source code for details)
|
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
|
It may raise an IOError exception according to the minimal level chosen
|
||||||
for the OleFileIO object.
|
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_UNSURE : a case which looks weird, but not sure it's a defect
|
||||||
DEFECT_POTENTIAL : a potential defect
|
DEFECT_POTENTIAL : a potential defect
|
||||||
DEFECT_INCORRECT : an error according to specifications, but parsing can go on
|
DEFECT_INCORRECT : an error according to specifications, but parsing can go on
|
||||||
DEFECT_FATAL : an error which cannot be ignored, parsing is impossible
|
DEFECT_FATAL : an error which cannot be ignored, parsing is impossible
|
||||||
message: string describing the defect, used with raised exception.
|
:param message: string describing the defect, used with raised exception.
|
||||||
exception_type: exception class to be raised, IOError by default
|
:param exception_type: exception class to be raised, IOError by default
|
||||||
"""
|
"""
|
||||||
# added by [PL]
|
# added by [PL]
|
||||||
if defect_level >= self._raise_defects_level:
|
if defect_level >= self._raise_defects_level:
|
||||||
|
@ -1089,7 +1093,7 @@ class OleFileIO:
|
||||||
Open an OLE2 file.
|
Open an OLE2 file.
|
||||||
Reads the header, FAT and directory.
|
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:
|
#[PL] check if filename is a string-like or file-like object:
|
||||||
# (it is better to check for a read() method)
|
# (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.
|
Checks if a stream has not been already referenced elsewhere.
|
||||||
This method should only be called once for each known stream, and only
|
This method should only be called once for each known stream, and only
|
||||||
if stream size is not null.
|
if stream size is not null.
|
||||||
first_sect: index of first sector of the stream in FAT
|
:param 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 minifat: if True, stream is located in the MiniFAT, else in the FAT
|
||||||
"""
|
"""
|
||||||
if minifat:
|
if minifat:
|
||||||
debug('_check_duplicate_stream: sect=%d in MiniFAT' % first_sect)
|
debug('_check_duplicate_stream: sect=%d in MiniFAT' % first_sect)
|
||||||
|
@ -1371,8 +1375,9 @@ class OleFileIO:
|
||||||
def loadfat_sect(self, sect):
|
def loadfat_sect(self, sect):
|
||||||
"""
|
"""
|
||||||
Adds the indexes of the given sector to the FAT
|
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.
|
# a FAT sector is an array of ulong integers.
|
||||||
if isinstance(sect, array.array):
|
if isinstance(sect, array.array):
|
||||||
|
@ -1505,8 +1510,9 @@ class OleFileIO:
|
||||||
def getsect(self, sect):
|
def getsect(self, sect):
|
||||||
"""
|
"""
|
||||||
Read given sector from file on disk.
|
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
|
# [PL] this original code was wrong when sectors are 4KB instead of
|
||||||
# 512 bytes:
|
# 512 bytes:
|
||||||
|
@ -1530,7 +1536,8 @@ class OleFileIO:
|
||||||
def loaddirectory(self, sect):
|
def loaddirectory(self, sect):
|
||||||
"""
|
"""
|
||||||
Load the directory.
|
Load the directory.
|
||||||
sect: sector index of directory stream.
|
|
||||||
|
:param sect: sector index of directory stream.
|
||||||
"""
|
"""
|
||||||
# The directory is stored in a standard
|
# The directory is stored in a standard
|
||||||
# substream, independent of its size.
|
# substream, independent of its size.
|
||||||
|
@ -1567,9 +1574,10 @@ class OleFileIO:
|
||||||
Load a directory entry from the directory.
|
Load a directory entry from the directory.
|
||||||
This method should only be called once for each storage/stream when
|
This method should only be called once for each storage/stream when
|
||||||
loading the directory.
|
loading the directory.
|
||||||
sid: index of storage/stream in the directory.
|
|
||||||
return: a _OleDirectoryEntry object
|
:param sid: index of storage/stream in the directory.
|
||||||
raise: IOError if the entry has always been referenced.
|
:returns: a _OleDirectoryEntry object
|
||||||
|
:exception IOError: if the entry has always been referenced.
|
||||||
"""
|
"""
|
||||||
# check if SID is OK:
|
# check if SID is OK:
|
||||||
if sid<0 or sid>=len(self.direntries):
|
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.
|
Open a stream, either in FAT or MiniFAT according to its size.
|
||||||
(openstream helper)
|
(openstream helper)
|
||||||
|
|
||||||
start: index of first sector
|
:param start: index of first sector
|
||||||
size: size of stream (or nothing if size is unknown)
|
:param size: size of stream (or nothing if size is unknown)
|
||||||
force_FAT: if False (default), stream will be opened in FAT or MiniFAT
|
: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.
|
according to size. If True, it will always be opened in FAT.
|
||||||
"""
|
"""
|
||||||
debug('OleFileIO.open(): sect=%d, size=%d, force_FAT=%s' %
|
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):
|
def _list(self, files, prefix, node, streams=True, storages=False):
|
||||||
"""
|
"""
|
||||||
(listdir helper)
|
(listdir helper)
|
||||||
files: list of files to fill in
|
:param files: list of files to fill in
|
||||||
prefix: current location in storage tree (list of names)
|
:param prefix: current location in storage tree (list of names)
|
||||||
node: current node (_OleDirectoryEntry object)
|
:param node: current node (_OleDirectoryEntry object)
|
||||||
streams: bool, include streams if True (True by default) - new in v0.26
|
:param 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 storages: bool, include storages if True (False by default) - new in v0.26
|
||||||
(note: the root storage is never included)
|
(note: the root storage is never included)
|
||||||
"""
|
"""
|
||||||
prefix = prefix + [node.name]
|
prefix = prefix + [node.name]
|
||||||
|
@ -1657,9 +1665,9 @@ class OleFileIO:
|
||||||
"""
|
"""
|
||||||
Return a list of streams stored in this file
|
Return a list of streams stored in this file
|
||||||
|
|
||||||
streams: bool, include streams if True (True by default) - new in v0.26
|
:param 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 storages: bool, include storages if True (False by default) - new in v0.26
|
||||||
(note: the root storage is never included)
|
(note: the root storage is never included)
|
||||||
"""
|
"""
|
||||||
files = []
|
files = []
|
||||||
self._list(files, [], self.root, streams, storages)
|
self._list(files, [], self.root, streams, storages)
|
||||||
|
@ -1671,12 +1679,13 @@ class OleFileIO:
|
||||||
Returns directory entry of given filename. (openstream helper)
|
Returns directory entry of given filename. (openstream helper)
|
||||||
Note: this method is case-insensitive.
|
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:
|
- a string using Unix path syntax, for example:
|
||||||
'storage_1/storage_1.2/stream'
|
'storage_1/storage_1.2/stream'
|
||||||
- a list of storage filenames, path to the desired stream/storage.
|
- a list of storage filenames, path to the desired stream/storage.
|
||||||
Example: ['storage_1', 'storage_1.2', 'stream']
|
Example: ['storage_1', 'storage_1.2', 'stream']
|
||||||
return: sid of requested filename
|
:returns: sid of requested filename
|
||||||
raise IOError if file not found
|
raise IOError if file not found
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@ -1700,13 +1709,15 @@ class OleFileIO:
|
||||||
"""
|
"""
|
||||||
Open a stream as a read-only file object (BytesIO).
|
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:
|
- a string using Unix path syntax, for example:
|
||||||
'storage_1/storage_1.2/stream'
|
'storage_1/storage_1.2/stream'
|
||||||
- a list of storage filenames, path to the desired stream/storage.
|
- a list of storage filenames, path to the desired stream/storage.
|
||||||
Example: ['storage_1', 'storage_1.2', 'stream']
|
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)
|
sid = self._find(filename)
|
||||||
entry = self.direntries[sid]
|
entry = self.direntries[sid]
|
||||||
|
@ -1720,8 +1731,9 @@ class OleFileIO:
|
||||||
Test if given filename exists as a stream or a storage in the OLE
|
Test if given filename exists as a stream or a storage in the OLE
|
||||||
container, and return its type.
|
container, and return its type.
|
||||||
|
|
||||||
filename: path of stream in storage tree. (see openstream for syntax)
|
:param filename: path of stream in storage tree. (see openstream for syntax)
|
||||||
return: False if object does not exist, its entry type (>0) otherwise:
|
:returns: False if object does not exist, its entry type (>0) otherwise:
|
||||||
|
|
||||||
- STGTY_STREAM: a stream
|
- STGTY_STREAM: a stream
|
||||||
- STGTY_STORAGE: a storage
|
- STGTY_STORAGE: a storage
|
||||||
- STGTY_ROOT: the root entry
|
- STGTY_ROOT: the root entry
|
||||||
|
@ -1738,10 +1750,10 @@ class OleFileIO:
|
||||||
"""
|
"""
|
||||||
Return modification time of a stream/storage.
|
Return modification time of a stream/storage.
|
||||||
|
|
||||||
filename: path of stream/storage in storage tree. (see openstream for
|
:param filename: path of stream/storage in storage tree. (see openstream for
|
||||||
syntax)
|
syntax)
|
||||||
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)
|
otherwise (UTC timezone)
|
||||||
|
|
||||||
new in version 0.26
|
new in version 0.26
|
||||||
"""
|
"""
|
||||||
|
@ -1754,10 +1766,10 @@ class OleFileIO:
|
||||||
"""
|
"""
|
||||||
Return creation time of a stream/storage.
|
Return creation time of a stream/storage.
|
||||||
|
|
||||||
filename: path of stream/storage in storage tree. (see openstream for
|
:param filename: path of stream/storage in storage tree. (see openstream for
|
||||||
syntax)
|
syntax)
|
||||||
return: None if creation time is null, a python datetime object
|
:returns: None if creation time is null, a python datetime object
|
||||||
otherwise (UTC timezone)
|
otherwise (UTC timezone)
|
||||||
|
|
||||||
new in version 0.26
|
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
|
Test if given filename exists as a stream or a storage in the OLE
|
||||||
container.
|
container.
|
||||||
|
|
||||||
filename: path of stream in storage tree. (see openstream for syntax)
|
:param filename: path of stream in storage tree. (see openstream for syntax)
|
||||||
return: True if object exist, else False.
|
:returns: True if object exist, else False.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
sid = self._find(filename)
|
sid = self._find(filename)
|
||||||
|
@ -1785,9 +1797,10 @@ class OleFileIO:
|
||||||
"""
|
"""
|
||||||
Return size of a stream in the OLE container, in bytes.
|
Return size of a stream in the OLE container, in bytes.
|
||||||
|
|
||||||
filename: path of stream in storage tree (see openstream for syntax)
|
:param filename: path of stream in storage tree (see openstream for syntax)
|
||||||
return: size in bytes (long integer)
|
:returns: size in bytes (long integer)
|
||||||
raise: IOError if file not found, TypeError if this is not a stream.
|
:exception IOError: if file not found
|
||||||
|
:exception TypeError: if this is not a stream
|
||||||
"""
|
"""
|
||||||
sid = self._find(filename)
|
sid = self._find(filename)
|
||||||
entry = self.direntries[sid]
|
entry = self.direntries[sid]
|
||||||
|
@ -1809,11 +1822,11 @@ class OleFileIO:
|
||||||
"""
|
"""
|
||||||
Return properties described in substream.
|
Return properties described in substream.
|
||||||
|
|
||||||
filename: path of stream in storage tree (see openstream for syntax)
|
:param filename: path of stream in storage tree (see openstream for syntax)
|
||||||
convert_time: bool, if True timestamps will be converted to Python datetime
|
:param convert_time: bool, if True timestamps will be converted to Python datetime
|
||||||
no_conversion: None or list of int, timestamps not to be converted
|
:param no_conversion: None or list of int, timestamps not to be converted
|
||||||
(for example total editing time is not a real timestamp)
|
(for example total editing time is not a real timestamp)
|
||||||
return: a dictionary of values indexed by id (integer)
|
:returns: a dictionary of values indexed by id (integer)
|
||||||
"""
|
"""
|
||||||
# make sure no_conversion is a list, just to simplify code below:
|
# make sure no_conversion is a list, just to simplify code below:
|
||||||
if no_conversion == None:
|
if no_conversion == None:
|
||||||
|
|
|
@ -73,9 +73,8 @@ class PSDraw:
|
||||||
|
|
||||||
def setink(self, ink):
|
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 ***")
|
print("*** NOT YET IMPLEMENTED ***")
|
||||||
|
|
||||||
|
|
|
@ -147,6 +147,17 @@ class ChunkStream:
|
||||||
|
|
||||||
return cids
|
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=))
|
# PNG chunk container (for use with save(pnginfo=))
|
||||||
|
@ -159,14 +170,36 @@ class PngInfo:
|
||||||
def add(self, cid, data):
|
def add(self, cid, data):
|
||||||
self.chunks.append((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):
|
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
|
# 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):
|
if not isinstance(key, bytes):
|
||||||
key = key.encode('latin-1', 'strict')
|
key = key.encode('latin-1', 'strict')
|
||||||
|
|
||||||
if not isinstance(value, bytes):
|
|
||||||
value = value.encode('latin-1', 'replace')
|
|
||||||
|
|
||||||
if zip:
|
if zip:
|
||||||
import zlib
|
import zlib
|
||||||
self.add(b"zTXt", key + b"\0\0" + zlib.compress(value))
|
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
|
self.im_info[k] = self.im_text[k] = v
|
||||||
return s
|
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
|
# PNG reader
|
||||||
|
|
||||||
|
|
|
@ -261,6 +261,7 @@ mode_map = {'1': _PyAccess8,
|
||||||
'PA': _PyAccess32_2,
|
'PA': _PyAccess32_2,
|
||||||
'RGB': _PyAccess32_3,
|
'RGB': _PyAccess32_3,
|
||||||
'LAB': _PyAccess32_3,
|
'LAB': _PyAccess32_3,
|
||||||
|
'HSV': _PyAccess32_3,
|
||||||
'YCbCr': _PyAccess32_3,
|
'YCbCr': _PyAccess32_3,
|
||||||
'RGBA': _PyAccess32_4,
|
'RGBA': _PyAccess32_4,
|
||||||
'RGBa': _PyAccess32_4,
|
'RGBa': _PyAccess32_4,
|
||||||
|
|
|
@ -31,6 +31,7 @@ i32 = _binary.i32be
|
||||||
def _accept(prefix):
|
def _accept(prefix):
|
||||||
return i16(prefix) == 474
|
return i16(prefix) == 474
|
||||||
|
|
||||||
|
|
||||||
##
|
##
|
||||||
# Image plugin for SGI images.
|
# Image plugin for SGI images.
|
||||||
|
|
||||||
|
@ -44,7 +45,7 @@ class SgiImageFile(ImageFile.ImageFile):
|
||||||
# HEAD
|
# HEAD
|
||||||
s = self.fp.read(512)
|
s = self.fp.read(512)
|
||||||
if i16(s) != 474:
|
if i16(s) != 474:
|
||||||
raise SyntaxError("not an SGI image file")
|
raise ValueError("Not an SGI image file")
|
||||||
|
|
||||||
# relevant header entries
|
# relevant header entries
|
||||||
compression = i8(s[2])
|
compression = i8(s[2])
|
||||||
|
@ -60,22 +61,22 @@ class SgiImageFile(ImageFile.ImageFile):
|
||||||
elif layout == (1, 3, 4):
|
elif layout == (1, 3, 4):
|
||||||
self.mode = "RGBA"
|
self.mode = "RGBA"
|
||||||
else:
|
else:
|
||||||
raise SyntaxError("unsupported SGI image mode")
|
raise ValueError("Unsupported SGI image mode")
|
||||||
|
|
||||||
# size
|
# size
|
||||||
self.size = i16(s[6:]), i16(s[8:])
|
self.size = i16(s[6:]), i16(s[8:])
|
||||||
|
|
||||||
|
|
||||||
# decoder info
|
# decoder info
|
||||||
if compression == 0:
|
if compression == 0:
|
||||||
offset = 512
|
offset = 512
|
||||||
pagesize = self.size[0]*self.size[1]*layout[0]
|
pagesize = self.size[0]*self.size[1]*layout[0]
|
||||||
self.tile = []
|
self.tile = []
|
||||||
for layer in self.mode:
|
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
|
offset = offset + pagesize
|
||||||
elif compression == 1:
|
elif compression == 1:
|
||||||
self.tile = [("sgi_rle", (0,0)+self.size, 512, (self.mode, 0, -1))]
|
raise ValueError("SGI RLE encoding not supported")
|
||||||
|
|
||||||
#
|
#
|
||||||
# registry
|
# registry
|
||||||
|
@ -85,5 +86,6 @@ Image.register_open("SGI", SgiImageFile, _accept)
|
||||||
Image.register_extension("SGI", ".bw")
|
Image.register_extension("SGI", ".bw")
|
||||||
Image.register_extension("SGI", ".rgb")
|
Image.register_extension("SGI", ".rgb")
|
||||||
Image.register_extension("SGI", ".rgba")
|
Image.register_extension("SGI", ".rgba")
|
||||||
|
Image.register_extension("SGI", ".sgi")
|
||||||
|
|
||||||
Image.register_extension("SGI", ".sgi") # really?
|
# End of file
|
||||||
|
|
|
@ -29,6 +29,7 @@ i32 = _binary.i32be
|
||||||
def _accept(prefix):
|
def _accept(prefix):
|
||||||
return i32(prefix) == 0x59a66a95
|
return i32(prefix) == 0x59a66a95
|
||||||
|
|
||||||
|
|
||||||
##
|
##
|
||||||
# Image plugin for Sun raster files.
|
# Image plugin for Sun raster files.
|
||||||
|
|
||||||
|
@ -70,9 +71,9 @@ class SunImageFile(ImageFile.ImageFile):
|
||||||
stride = (((self.size[0] * depth + 7) // 8) + 3) & (~3)
|
stride = (((self.size[0] * depth + 7) // 8) + 3) & (~3)
|
||||||
|
|
||||||
if compression == 1:
|
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:
|
elif compression == 2:
|
||||||
self.tile = [("sun_rle", (0,0)+self.size, offset, rawmode)]
|
self.tile = [("sun_rle", (0, 0)+self.size, offset, rawmode)]
|
||||||
|
|
||||||
#
|
#
|
||||||
# registry
|
# registry
|
||||||
|
|
|
@ -42,9 +42,6 @@ MODES = {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def _accept(prefix):
|
|
||||||
return prefix[0:1] == b"\0"
|
|
||||||
|
|
||||||
##
|
##
|
||||||
# Image plugin for Targa files.
|
# Image plugin for Targa files.
|
||||||
|
|
||||||
|
@ -58,7 +55,7 @@ class TgaImageFile(ImageFile.ImageFile):
|
||||||
# process header
|
# process header
|
||||||
s = self.fp.read(18)
|
s = self.fp.read(18)
|
||||||
|
|
||||||
id = i8(s[0])
|
idlen = i8(s[0])
|
||||||
|
|
||||||
colormaptype = i8(s[1])
|
colormaptype = i8(s[1])
|
||||||
imagetype = i8(s[2])
|
imagetype = i8(s[2])
|
||||||
|
@ -70,7 +67,7 @@ class TgaImageFile(ImageFile.ImageFile):
|
||||||
self.size = i16(s[12:]), i16(s[14:])
|
self.size = i16(s[12:]), i16(s[14:])
|
||||||
|
|
||||||
# validate header fields
|
# 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\
|
self.size[0] <= 0 or self.size[1] <= 0 or\
|
||||||
depth not in (1, 8, 16, 24, 32):
|
depth not in (1, 8, 16, 24, 32):
|
||||||
raise SyntaxError("not a TGA file")
|
raise SyntaxError("not a TGA file")
|
||||||
|
@ -79,7 +76,7 @@ class TgaImageFile(ImageFile.ImageFile):
|
||||||
if imagetype in (3, 11):
|
if imagetype in (3, 11):
|
||||||
self.mode = "L"
|
self.mode = "L"
|
||||||
if depth == 1:
|
if depth == 1:
|
||||||
self.mode = "1" # ???
|
self.mode = "1" # ???
|
||||||
elif imagetype in (1, 9):
|
elif imagetype in (1, 9):
|
||||||
self.mode = "P"
|
self.mode = "P"
|
||||||
elif imagetype in (2, 10):
|
elif imagetype in (2, 10):
|
||||||
|
@ -103,22 +100,25 @@ class TgaImageFile(ImageFile.ImageFile):
|
||||||
if imagetype & 8:
|
if imagetype & 8:
|
||||||
self.info["compression"] = "tga_rle"
|
self.info["compression"] = "tga_rle"
|
||||||
|
|
||||||
|
if idlen:
|
||||||
|
self.info["id_section"] = self.fp.read(idlen)
|
||||||
|
|
||||||
if colormaptype:
|
if colormaptype:
|
||||||
# read palette
|
# read palette
|
||||||
start, size, mapdepth = i16(s[3:]), i16(s[5:]), i16(s[7:])
|
start, size, mapdepth = i16(s[3:]), i16(s[5:]), i16(s[7:])
|
||||||
if mapdepth == 16:
|
if mapdepth == 16:
|
||||||
self.palette = ImagePalette.raw("BGR;16",
|
self.palette = ImagePalette.raw(
|
||||||
b"\0"*2*start + self.fp.read(2*size))
|
"BGR;16", b"\0"*2*start + self.fp.read(2*size))
|
||||||
elif mapdepth == 24:
|
elif mapdepth == 24:
|
||||||
self.palette = ImagePalette.raw("BGR",
|
self.palette = ImagePalette.raw(
|
||||||
b"\0"*3*start + self.fp.read(3*size))
|
"BGR", b"\0"*3*start + self.fp.read(3*size))
|
||||||
elif mapdepth == 32:
|
elif mapdepth == 32:
|
||||||
self.palette = ImagePalette.raw("BGRA",
|
self.palette = ImagePalette.raw(
|
||||||
b"\0"*4*start + self.fp.read(4*size))
|
"BGRA", b"\0"*4*start + self.fp.read(4*size))
|
||||||
|
|
||||||
# setup tile descriptor
|
# setup tile descriptor
|
||||||
try:
|
try:
|
||||||
rawmode = MODES[(imagetype&7, depth)]
|
rawmode = MODES[(imagetype & 7, depth)]
|
||||||
if imagetype & 8:
|
if imagetype & 8:
|
||||||
# compressed
|
# compressed
|
||||||
self.tile = [("tga_rle", (0, 0)+self.size,
|
self.tile = [("tga_rle", (0, 0)+self.size,
|
||||||
|
@ -127,7 +127,7 @@ class TgaImageFile(ImageFile.ImageFile):
|
||||||
self.tile = [("raw", (0, 0)+self.size,
|
self.tile = [("raw", (0, 0)+self.size,
|
||||||
self.fp.tell(), (rawmode, 0, orientation))]
|
self.fp.tell(), (rawmode, 0, orientation))]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
pass # cannot decode
|
pass # cannot decode
|
||||||
|
|
||||||
#
|
#
|
||||||
# --------------------------------------------------------------------
|
# --------------------------------------------------------------------
|
||||||
|
@ -145,6 +145,7 @@ SAVE = {
|
||||||
"RGBA": ("BGRA", 32, 0, 2),
|
"RGBA": ("BGRA", 32, 0, 2),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def _save(im, fp, filename, check=0):
|
def _save(im, fp, filename, check=0):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -185,13 +186,14 @@ def _save(im, fp, filename, check=0):
|
||||||
if colormaptype:
|
if colormaptype:
|
||||||
fp.write(im.im.getpalette("RGB", "BGR"))
|
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
|
# Registry
|
||||||
|
|
||||||
Image.register_open("TGA", TgaImageFile, _accept)
|
Image.register_open("TGA", TgaImageFile)
|
||||||
Image.register_save("TGA", _save)
|
Image.register_save("TGA", _save)
|
||||||
|
|
||||||
Image.register_extension("TGA", ".tga")
|
Image.register_extension("TGA", ".tga")
|
||||||
|
|
|
@ -49,17 +49,18 @@ from PIL import _binary
|
||||||
from PIL._util import isStringType
|
from PIL._util import isStringType
|
||||||
|
|
||||||
import warnings
|
import warnings
|
||||||
import array, sys
|
import array
|
||||||
|
import sys
|
||||||
import collections
|
import collections
|
||||||
import itertools
|
import itertools
|
||||||
import os
|
import os
|
||||||
|
|
||||||
# Set these to true to force use of libtiff for reading or writing.
|
# Set these to true to force use of libtiff for reading or writing.
|
||||||
READ_LIBTIFF = False
|
READ_LIBTIFF = False
|
||||||
WRITE_LIBTIFF= False
|
WRITE_LIBTIFF = False
|
||||||
|
|
||||||
II = b"II" # little-endian (intel-style)
|
II = b"II" # little-endian (Intel style)
|
||||||
MM = b"MM" # big-endian (motorola-style)
|
MM = b"MM" # big-endian (Motorola style)
|
||||||
|
|
||||||
i8 = _binary.i8
|
i8 = _binary.i8
|
||||||
o8 = _binary.o8
|
o8 = _binary.o8
|
||||||
|
@ -109,8 +110,8 @@ EXTRASAMPLES = 338
|
||||||
SAMPLEFORMAT = 339
|
SAMPLEFORMAT = 339
|
||||||
JPEGTABLES = 347
|
JPEGTABLES = 347
|
||||||
COPYRIGHT = 33432
|
COPYRIGHT = 33432
|
||||||
IPTC_NAA_CHUNK = 33723 # newsphoto properties
|
IPTC_NAA_CHUNK = 33723 # newsphoto properties
|
||||||
PHOTOSHOP_CHUNK = 34377 # photoshop properties
|
PHOTOSHOP_CHUNK = 34377 # photoshop properties
|
||||||
ICCPROFILE = 34675
|
ICCPROFILE = 34675
|
||||||
EXIFIFD = 34665
|
EXIFIFD = 34665
|
||||||
XMP = 700
|
XMP = 700
|
||||||
|
@ -126,10 +127,10 @@ COMPRESSION_INFO = {
|
||||||
3: "group3",
|
3: "group3",
|
||||||
4: "group4",
|
4: "group4",
|
||||||
5: "tiff_lzw",
|
5: "tiff_lzw",
|
||||||
6: "tiff_jpeg", # obsolete
|
6: "tiff_jpeg", # obsolete
|
||||||
7: "jpeg",
|
7: "jpeg",
|
||||||
8: "tiff_adobe_deflate",
|
8: "tiff_adobe_deflate",
|
||||||
32771: "tiff_raw_16", # 16-bit padding
|
32771: "tiff_raw_16", # 16-bit padding
|
||||||
32773: "packbits",
|
32773: "packbits",
|
||||||
32809: "tiff_thunderscan",
|
32809: "tiff_thunderscan",
|
||||||
32946: "tiff_deflate",
|
32946: "tiff_deflate",
|
||||||
|
@ -137,7 +138,7 @@ COMPRESSION_INFO = {
|
||||||
34677: "tiff_sgilog24",
|
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 = {
|
OPEN_INFO = {
|
||||||
# (ByteOrder, PhotoInterpretation, SampleFormat, FillOrder, BitsPerSample,
|
# (ByteOrder, PhotoInterpretation, SampleFormat, FillOrder, BitsPerSample,
|
||||||
|
@ -150,7 +151,7 @@ OPEN_INFO = {
|
||||||
(II, 1, 1, 1, (1,), ()): ("1", "1"),
|
(II, 1, 1, 1, (1,), ()): ("1", "1"),
|
||||||
(II, 1, 1, 2, (1,), ()): ("1", "1;R"),
|
(II, 1, 1, 2, (1,), ()): ("1", "1;R"),
|
||||||
(II, 1, 1, 1, (8,), ()): ("L", "L"),
|
(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, 2, (8,), ()): ("L", "L;R"),
|
||||||
(II, 1, 1, 1, (12,), ()): ("I;16", "I;12"),
|
(II, 1, 1, 1, (12,), ()): ("I;16", "I;12"),
|
||||||
(II, 1, 1, 1, (16,), ()): ("I;16", "I;16"),
|
(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, 1, 1, (32,), ()): ("I", "I;32N"),
|
||||||
(II, 1, 2, 1, (32,), ()): ("I", "I;32S"),
|
(II, 1, 2, 1, (32,), ()): ("I", "I;32S"),
|
||||||
(II, 1, 3, 1, (32,), ()): ("F", "F;32F"),
|
(II, 1, 3, 1, (32,), ()): ("F", "F;32F"),
|
||||||
(II, 2, 1, 1, (8,8,8), ()): ("RGB", "RGB"),
|
(II, 2, 1, 1, (8, 8, 8), ()): ("RGB", "RGB"),
|
||||||
(II, 2, 1, 2, (8,8,8), ()): ("RGB", "RGB;R"),
|
(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), ()): ("RGBA", "RGBA"), # missing ExtraSamples
|
||||||
(II, 2, 1, 1, (8,8,8,8), (0,)): ("RGBX", "RGBX"),
|
(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), (1,)): ("RGBA", "RGBa"),
|
||||||
(II, 2, 1, 1, (8,8,8,8), (2,)): ("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, 8), (999,)): ("RGBA", "RGBA"), # Corel Draw 10
|
||||||
(II, 3, 1, 1, (1,), ()): ("P", "P;1"),
|
(II, 3, 1, 1, (1,), ()): ("P", "P;1"),
|
||||||
(II, 3, 1, 2, (1,), ()): ("P", "P;1R"),
|
(II, 3, 1, 2, (1,), ()): ("P", "P;1R"),
|
||||||
(II, 3, 1, 1, (2,), ()): ("P", "P;2"),
|
(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, 1, (4,), ()): ("P", "P;4"),
|
||||||
(II, 3, 1, 2, (4,), ()): ("P", "P;4R"),
|
(II, 3, 1, 2, (4,), ()): ("P", "P;4R"),
|
||||||
(II, 3, 1, 1, (8,), ()): ("P", "P"),
|
(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, 3, 1, 2, (8,), ()): ("P", "P;R"),
|
||||||
(II, 5, 1, 1, (8,8,8,8), ()): ("CMYK", "CMYK"),
|
(II, 5, 1, 1, (8, 8, 8, 8), ()): ("CMYK", "CMYK"),
|
||||||
(II, 6, 1, 1, (8,8,8), ()): ("YCbCr", "YCbCr"),
|
(II, 6, 1, 1, (8, 8, 8), ()): ("YCbCr", "YCbCr"),
|
||||||
(II, 8, 1, 1, (8,8,8), ()): ("LAB", "LAB"),
|
(II, 8, 1, 1, (8, 8, 8), ()): ("LAB", "LAB"),
|
||||||
|
|
||||||
(MM, 0, 1, 1, (1,), ()): ("1", "1;I"),
|
(MM, 0, 1, 1, (1,), ()): ("1", "1;I"),
|
||||||
(MM, 0, 1, 2, (1,), ()): ("1", "1;IR"),
|
(MM, 0, 1, 2, (1,), ()): ("1", "1;IR"),
|
||||||
|
@ -185,18 +186,18 @@ OPEN_INFO = {
|
||||||
(MM, 1, 1, 1, (1,), ()): ("1", "1"),
|
(MM, 1, 1, 1, (1,), ()): ("1", "1"),
|
||||||
(MM, 1, 1, 2, (1,), ()): ("1", "1;R"),
|
(MM, 1, 1, 2, (1,), ()): ("1", "1;R"),
|
||||||
(MM, 1, 1, 1, (8,), ()): ("L", "L"),
|
(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, 2, (8,), ()): ("L", "L;R"),
|
||||||
(MM, 1, 1, 1, (16,), ()): ("I;16B", "I;16B"),
|
(MM, 1, 1, 1, (16,), ()): ("I;16B", "I;16B"),
|
||||||
(MM, 1, 2, 1, (16,), ()): ("I;16BS", "I;16BS"),
|
(MM, 1, 2, 1, (16,), ()): ("I;16BS", "I;16BS"),
|
||||||
(MM, 1, 2, 1, (32,), ()): ("I;32BS", "I;32BS"),
|
(MM, 1, 2, 1, (32,), ()): ("I;32BS", "I;32BS"),
|
||||||
(MM, 1, 3, 1, (32,), ()): ("F", "F;32BF"),
|
(MM, 1, 3, 1, (32,), ()): ("F", "F;32BF"),
|
||||||
(MM, 2, 1, 1, (8,8,8), ()): ("RGB", "RGB"),
|
(MM, 2, 1, 1, (8, 8, 8), ()): ("RGB", "RGB"),
|
||||||
(MM, 2, 1, 2, (8,8,8), ()): ("RGB", "RGB;R"),
|
(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), (0,)): ("RGBX", "RGBX"),
|
||||||
(MM, 2, 1, 1, (8,8,8,8), (1,)): ("RGBA", "RGBa"),
|
(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), (2,)): ("RGBA", "RGBA"),
|
||||||
(MM, 2, 1, 1, (8,8,8,8), (999,)): ("RGBA", "RGBA"), # corel draw 10
|
(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, 1, (1,), ()): ("P", "P;1"),
|
||||||
(MM, 3, 1, 2, (1,), ()): ("P", "P;1R"),
|
(MM, 3, 1, 2, (1,), ()): ("P", "P;1R"),
|
||||||
(MM, 3, 1, 1, (2,), ()): ("P", "P;2"),
|
(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, 1, (4,), ()): ("P", "P;4"),
|
||||||
(MM, 3, 1, 2, (4,), ()): ("P", "P;4R"),
|
(MM, 3, 1, 2, (4,), ()): ("P", "P;4R"),
|
||||||
(MM, 3, 1, 1, (8,), ()): ("P", "P"),
|
(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, 3, 1, 2, (8,), ()): ("P", "P;R"),
|
||||||
(MM, 5, 1, 1, (8,8,8,8), ()): ("CMYK", "CMYK"),
|
(MM, 5, 1, 1, (8, 8, 8, 8), ()): ("CMYK", "CMYK"),
|
||||||
(MM, 6, 1, 1, (8,8,8), ()): ("YCbCr", "YCbCr"),
|
(MM, 6, 1, 1, (8, 8, 8), ()): ("YCbCr", "YCbCr"),
|
||||||
(MM, 8, 1, 1, (8,8,8), ()): ("LAB", "LAB"),
|
(MM, 8, 1, 1, (8, 8, 8), ()): ("LAB", "LAB"),
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
PREFIXES = [b"MM\000\052", b"II\052\000", b"II\xBC\000"]
|
PREFIXES = [b"MM\000\052", b"II\052\000", b"II\xBC\000"]
|
||||||
|
|
||||||
|
|
||||||
def _accept(prefix):
|
def _accept(prefix):
|
||||||
return prefix[:4] in PREFIXES
|
return prefix[:4] in PREFIXES
|
||||||
|
|
||||||
|
|
||||||
##
|
##
|
||||||
# Wrapper for TIFF IFDs.
|
# Wrapper for TIFF IFDs.
|
||||||
|
|
||||||
|
@ -276,8 +279,9 @@ class ImageFileDirectory(collections.MutableMapping):
|
||||||
#: For a complete dictionary, use the as_dict method.
|
#: For a complete dictionary, use the as_dict method.
|
||||||
self.tags = {}
|
self.tags = {}
|
||||||
self.tagdata = {}
|
self.tagdata = {}
|
||||||
self.tagtype = {} # added 2008-06-05 by Florian Hoech
|
self.tagtype = {} # added 2008-06-05 by Florian Hoech
|
||||||
self.next = None
|
self.next = None
|
||||||
|
self.offset = None
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return str(self.as_dict())
|
return str(self.as_dict())
|
||||||
|
@ -287,7 +291,9 @@ class ImageFileDirectory(collections.MutableMapping):
|
||||||
return dict(self.items())
|
return dict(self.items())
|
||||||
|
|
||||||
def named(self):
|
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
|
from PIL import TiffTags
|
||||||
result = {}
|
result = {}
|
||||||
for tag_code, value in self.items():
|
for tag_code, value in self.items():
|
||||||
|
@ -295,7 +301,6 @@ class ImageFileDirectory(collections.MutableMapping):
|
||||||
result[tag_name] = value
|
result[tag_name] = value
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
# dictionary API
|
# dictionary API
|
||||||
|
|
||||||
def __len__(self):
|
def __len__(self):
|
||||||
|
@ -305,7 +310,7 @@ class ImageFileDirectory(collections.MutableMapping):
|
||||||
try:
|
try:
|
||||||
return self.tags[tag]
|
return self.tags[tag]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
data = self.tagdata[tag] # unpack on the fly
|
data = self.tagdata[tag] # unpack on the fly
|
||||||
type = self.tagtype[tag]
|
type = self.tagtype[tag]
|
||||||
size, handler = self.load_dispatch[type]
|
size, handler = self.load_dispatch[type]
|
||||||
self.tags[tag] = data = handler(self, data)
|
self.tags[tag] = data = handler(self, data)
|
||||||
|
@ -319,7 +324,7 @@ class ImageFileDirectory(collections.MutableMapping):
|
||||||
if tag == SAMPLEFORMAT:
|
if tag == SAMPLEFORMAT:
|
||||||
# work around broken (?) matrox library
|
# work around broken (?) matrox library
|
||||||
# (from Ted Wright, via Bob Klimek)
|
# (from Ted Wright, via Bob Klimek)
|
||||||
raise KeyError # use default
|
raise KeyError # use default
|
||||||
raise ValueError("not a scalar")
|
raise ValueError("not a scalar")
|
||||||
return value[0]
|
return value[0]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
|
@ -411,6 +416,7 @@ class ImageFileDirectory(collections.MutableMapping):
|
||||||
# load tag dictionary
|
# load tag dictionary
|
||||||
|
|
||||||
self.reset()
|
self.reset()
|
||||||
|
self.offset = fp.tell()
|
||||||
|
|
||||||
i16 = self.i16
|
i16 = self.i16
|
||||||
i32 = self.i32
|
i32 = self.i32
|
||||||
|
@ -433,7 +439,7 @@ class ImageFileDirectory(collections.MutableMapping):
|
||||||
except KeyError:
|
except KeyError:
|
||||||
if Image.DEBUG:
|
if Image.DEBUG:
|
||||||
print("- unsupported type", typ)
|
print("- unsupported type", typ)
|
||||||
continue # ignore unsupported type
|
continue # ignore unsupported type
|
||||||
|
|
||||||
size, handler = dispatch
|
size, handler = dispatch
|
||||||
|
|
||||||
|
@ -442,21 +448,28 @@ class ImageFileDirectory(collections.MutableMapping):
|
||||||
# Get and expand tag value
|
# Get and expand tag value
|
||||||
if size > 4:
|
if size > 4:
|
||||||
here = fp.tell()
|
here = fp.tell()
|
||||||
|
if Image.DEBUG:
|
||||||
|
print ("Tag Location: %s" %here)
|
||||||
fp.seek(i32(ifd, 8))
|
fp.seek(i32(ifd, 8))
|
||||||
|
if Image.DEBUG:
|
||||||
|
print ("Data Location: %s" %fp.tell())
|
||||||
data = ImageFile._safe_read(fp, size)
|
data = ImageFile._safe_read(fp, size)
|
||||||
fp.seek(here)
|
fp.seek(here)
|
||||||
else:
|
else:
|
||||||
data = ifd[8:8+size]
|
data = ifd[8:8+size]
|
||||||
|
|
||||||
if len(data) != 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
|
continue
|
||||||
|
|
||||||
self.tagdata[tag] = data
|
self.tagdata[tag] = data
|
||||||
self.tagtype[tag] = typ
|
self.tagtype[tag] = typ
|
||||||
|
|
||||||
if Image.DEBUG:
|
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: <table: %d bytes>" % size)
|
print("- value: <table: %d bytes>" % size)
|
||||||
else:
|
else:
|
||||||
print("- value:", self[tag])
|
print("- value:", self[tag])
|
||||||
|
@ -518,8 +531,8 @@ class ImageFileDirectory(collections.MutableMapping):
|
||||||
# integer data
|
# integer data
|
||||||
if tag == STRIPOFFSETS:
|
if tag == STRIPOFFSETS:
|
||||||
stripoffsets = len(directory)
|
stripoffsets = len(directory)
|
||||||
typ = 4 # to avoid catch-22
|
typ = 4 # to avoid catch-22
|
||||||
elif tag in (X_RESOLUTION, Y_RESOLUTION) or typ==5:
|
elif tag in (X_RESOLUTION, Y_RESOLUTION) or typ == 5:
|
||||||
# identify rational data fields
|
# identify rational data fields
|
||||||
typ = 5
|
typ = 5
|
||||||
if isinstance(value[0], tuple):
|
if isinstance(value[0], tuple):
|
||||||
|
@ -541,7 +554,8 @@ class ImageFileDirectory(collections.MutableMapping):
|
||||||
typname = TiffTags.TYPES.get(typ, "unknown")
|
typname = TiffTags.TYPES.get(typ, "unknown")
|
||||||
print("save: %s (%d)" % (tagname, tag), end=' ')
|
print("save: %s (%d)" % (tagname, tag), end=' ')
|
||||||
print("- type: %s (%d)" % (typname, typ), 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)
|
size = len(data)
|
||||||
print("- value: <table: %d bytes>" % size)
|
print("- value: <table: %d bytes>" % size)
|
||||||
else:
|
else:
|
||||||
|
@ -576,7 +590,7 @@ class ImageFileDirectory(collections.MutableMapping):
|
||||||
fp.write(o16(tag) + o16(typ) + o32(count) + value)
|
fp.write(o16(tag) + o16(typ) + o32(count) + value)
|
||||||
|
|
||||||
# -- overwrite here for multi-page --
|
# -- 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
|
# pass 3: write auxiliary data to file
|
||||||
for tag, typ, count, value, data in directory:
|
for tag, typ, count, value, data in directory:
|
||||||
|
@ -586,6 +600,7 @@ class ImageFileDirectory(collections.MutableMapping):
|
||||||
|
|
||||||
return offset
|
return offset
|
||||||
|
|
||||||
|
|
||||||
##
|
##
|
||||||
# Image plugin for TIFF files.
|
# Image plugin for TIFF files.
|
||||||
|
|
||||||
|
@ -616,23 +631,25 @@ class TiffImageFile(ImageFile.ImageFile):
|
||||||
print ("- __first:", self.__first)
|
print ("- __first:", self.__first)
|
||||||
print ("- ifh: ", ifh)
|
print ("- ifh: ", ifh)
|
||||||
|
|
||||||
# and load the first frame
|
# and load the first frame
|
||||||
self._seek(0)
|
self._seek(0)
|
||||||
|
|
||||||
def seek(self, frame):
|
def seek(self, frame):
|
||||||
"Select a given frame as current image"
|
"Select a given frame as current image"
|
||||||
|
|
||||||
if frame < 0:
|
if frame < 0:
|
||||||
frame = 0
|
frame = 0
|
||||||
self._seek(frame)
|
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):
|
def tell(self):
|
||||||
"Return the current frame number"
|
"Return the current frame number"
|
||||||
|
|
||||||
return self._tell()
|
return self._tell()
|
||||||
|
|
||||||
def _seek(self, frame):
|
def _seek(self, frame):
|
||||||
|
|
||||||
self.fp = self.__fp
|
self.fp = self.__fp
|
||||||
if frame < self.__frame:
|
if frame < self.__frame:
|
||||||
# rewind file
|
# rewind file
|
||||||
|
@ -641,14 +658,21 @@ class TiffImageFile(ImageFile.ImageFile):
|
||||||
while self.__frame < frame:
|
while self.__frame < frame:
|
||||||
if not self.__next:
|
if not self.__next:
|
||||||
raise EOFError("no more images in TIFF file")
|
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)
|
self.fp.seek(self.__next)
|
||||||
|
if Image.DEBUG:
|
||||||
|
print("Loading tags, location: %s"%self.fp.tell())
|
||||||
self.tag.load(self.fp)
|
self.tag.load(self.fp)
|
||||||
self.__next = self.tag.next
|
self.__next = self.tag.next
|
||||||
self.__frame += 1
|
self.__frame += 1
|
||||||
self._setup()
|
self._setup()
|
||||||
|
|
||||||
def _tell(self):
|
def _tell(self):
|
||||||
|
|
||||||
return self.__frame
|
return self.__frame
|
||||||
|
|
||||||
def _decoder(self, rawmode, layer, tile=None):
|
def _decoder(self, rawmode, layer, tile=None):
|
||||||
|
@ -694,9 +718,12 @@ class TiffImageFile(ImageFile.ImageFile):
|
||||||
if not len(self.tile) == 1:
|
if not len(self.tile) == 1:
|
||||||
raise IOError("Not exactly one tile")
|
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]
|
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:
|
try:
|
||||||
decoder.setimage(self.im, extents)
|
decoder.setimage(self.im, extents)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
|
@ -706,35 +733,36 @@ class TiffImageFile(ImageFile.ImageFile):
|
||||||
# We've got a stringio like thing passed in. Yay for all in memory.
|
# 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
|
# 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.
|
# 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
|
# unless we could do something like get the address of the
|
||||||
# string for stringio.
|
# underlying string for stringio.
|
||||||
#
|
#
|
||||||
# Rearranging for supporting byteio items, since they have a fileno
|
# Rearranging for supporting byteio items, since they have a fileno
|
||||||
# that returns an IOError if there's no underlying fp. Easier to deal
|
# that returns an IOError if there's no underlying fp. Easier to
|
||||||
# with here by reordering.
|
# dea. with here by reordering.
|
||||||
if Image.DEBUG:
|
if Image.DEBUG:
|
||||||
print ("have getvalue. just sending in a string from getvalue")
|
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"):
|
elif hasattr(self.fp, "fileno"):
|
||||||
# we've got a actual file on disk, pass in the fp.
|
# we've got a actual file on disk, pass in the fp.
|
||||||
if Image.DEBUG:
|
if Image.DEBUG:
|
||||||
print ("have fileno, calling fileno version of the decoder.")
|
print ("have fileno, calling fileno version of the decoder.")
|
||||||
self.fp.seek(0)
|
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:
|
else:
|
||||||
# we have something else.
|
# we have something else.
|
||||||
if Image.DEBUG:
|
if Image.DEBUG:
|
||||||
print ("don't have fileno or getvalue. just reading")
|
print ("don't have fileno or getvalue. just reading")
|
||||||
# UNDONE -- so much for that buffer size thing.
|
# 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.tile = []
|
||||||
self.readonly = 0
|
self.readonly = 0
|
||||||
# libtiff closed the fp in a, we need to close self.fp, if possible
|
# libtiff closed the fp in a, we need to close self.fp, if possible
|
||||||
if hasattr(self.fp, 'close'):
|
if hasattr(self.fp, 'close'):
|
||||||
self.fp.close()
|
if not self.__next:
|
||||||
self.fp = None # might be shared
|
self.fp.close()
|
||||||
|
self.fp = None # might be shared
|
||||||
|
|
||||||
if err < 0:
|
if err < 0:
|
||||||
raise IOError(err)
|
raise IOError(err)
|
||||||
|
@ -810,11 +838,11 @@ class TiffImageFile(ImageFile.ImageFile):
|
||||||
xres = xres[0] / (xres[1] or 1)
|
xres = xres[0] / (xres[1] or 1)
|
||||||
yres = yres[0] / (yres[1] or 1)
|
yres = yres[0] / (yres[1] or 1)
|
||||||
resunit = getscalar(RESOLUTION_UNIT, 1)
|
resunit = getscalar(RESOLUTION_UNIT, 1)
|
||||||
if resunit == 2: # dots per inch
|
if resunit == 2: # dots per inch
|
||||||
self.info["dpi"] = xres, yres
|
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
|
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
|
self.info["resolution"] = xres, yres
|
||||||
|
|
||||||
# build tile descriptors
|
# build tile descriptors
|
||||||
|
@ -825,13 +853,16 @@ class TiffImageFile(ImageFile.ImageFile):
|
||||||
offsets = self.tag[STRIPOFFSETS]
|
offsets = self.tag[STRIPOFFSETS]
|
||||||
h = getscalar(ROWSPERSTRIP, ysize)
|
h = getscalar(ROWSPERSTRIP, ysize)
|
||||||
w = self.size[0]
|
w = self.size[0]
|
||||||
if READ_LIBTIFF or self._compression in ["tiff_ccitt", "group3", "group4",
|
if READ_LIBTIFF or self._compression in ["tiff_ccitt", "group3",
|
||||||
"tiff_jpeg", "tiff_adobe_deflate",
|
"group4", "tiff_jpeg",
|
||||||
"tiff_thunderscan", "tiff_deflate",
|
"tiff_adobe_deflate",
|
||||||
"tiff_sgilog", "tiff_sgilog24",
|
"tiff_thunderscan",
|
||||||
|
"tiff_deflate",
|
||||||
|
"tiff_sgilog",
|
||||||
|
"tiff_sgilog24",
|
||||||
"tiff_raw_16"]:
|
"tiff_raw_16"]:
|
||||||
## if Image.DEBUG:
|
# if Image.DEBUG:
|
||||||
## print "Activating g4 compression for whole file"
|
# print "Activating g4 compression for whole file"
|
||||||
|
|
||||||
# Decoder expects entire file as one tile.
|
# Decoder expects entire file as one tile.
|
||||||
# There's a buffer size limit in load (64k)
|
# 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.
|
# libtiff closes the file descriptor, so pass in a dup.
|
||||||
try:
|
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:
|
except IOError:
|
||||||
# io.BytesIO have a fileno, but returns an IOError if
|
# io.BytesIO have a fileno, but returns an IOError if
|
||||||
# it doesn't use a file descriptor.
|
# 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
|
# Offset in the tile tuple is 0, we go from 0,0 to
|
||||||
# w,h, and we only do this once -- eds
|
# w,h, and we only do this once -- eds
|
||||||
a = (rawmode, self._compression, fp )
|
a = (rawmode, self._compression, fp)
|
||||||
self.tile.append(
|
self.tile.append(
|
||||||
(self._compression,
|
(self._compression,
|
||||||
(0, 0, w, ysize),
|
(0, 0, w, ysize),
|
||||||
|
@ -893,8 +925,8 @@ class TiffImageFile(ImageFile.ImageFile):
|
||||||
a = self._decoder(rawmode, l, i)
|
a = self._decoder(rawmode, l, i)
|
||||||
self.tile.append(
|
self.tile.append(
|
||||||
(self._compression,
|
(self._compression,
|
||||||
(0, min(y, ysize), w, min(y+h, ysize)),
|
(0, min(y, ysize), w, min(y+h, ysize)),
|
||||||
offsets[i], a))
|
offsets[i], a))
|
||||||
if Image.DEBUG:
|
if Image.DEBUG:
|
||||||
print ("tiles: ", self.tile)
|
print ("tiles: ", self.tile)
|
||||||
y = y + h
|
y = y + h
|
||||||
|
@ -914,8 +946,8 @@ class TiffImageFile(ImageFile.ImageFile):
|
||||||
# is not a multiple of the tile size...
|
# is not a multiple of the tile size...
|
||||||
self.tile.append(
|
self.tile.append(
|
||||||
(self._compression,
|
(self._compression,
|
||||||
(x, y, x+w, y+h),
|
(x, y, x+w, y+h),
|
||||||
o, a))
|
o, a))
|
||||||
x = x + w
|
x = x + w
|
||||||
if x >= self.size[0]:
|
if x >= self.size[0]:
|
||||||
x, y = 0, y + h
|
x, y = 0, y + h
|
||||||
|
@ -937,25 +969,27 @@ class TiffImageFile(ImageFile.ImageFile):
|
||||||
# --------------------------------------------------------------------
|
# --------------------------------------------------------------------
|
||||||
# Write TIFF files
|
# 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 = {
|
SAVE_INFO = {
|
||||||
# mode => rawmode, byteorder, photometrics, sampleformat, bitspersample, extra
|
# mode => rawmode, byteorder, photometrics,
|
||||||
|
# sampleformat, bitspersample, extra
|
||||||
"1": ("1", II, 1, 1, (1,), None),
|
"1": ("1", II, 1, 1, (1,), None),
|
||||||
"L": ("L", II, 1, 1, (8,), 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),
|
"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": ("I;32S", II, 1, 2, (32,), None),
|
||||||
"I;16": ("I;16", II, 1, 1, (16,), None),
|
"I;16": ("I;16", II, 1, 1, (16,), None),
|
||||||
"I;16S": ("I;16S", II, 1, 2, (16,), None),
|
"I;16S": ("I;16S", II, 1, 2, (16,), None),
|
||||||
"F": ("F;32F", II, 1, 3, (32,), None),
|
"F": ("F;32F", II, 1, 3, (32,), None),
|
||||||
"RGB": ("RGB", II, 2, 1, (8,8,8), None),
|
"RGB": ("RGB", II, 2, 1, (8, 8, 8), None),
|
||||||
"RGBX": ("RGBX", II, 2, 1, (8,8,8,8), 0),
|
"RGBX": ("RGBX", II, 2, 1, (8, 8, 8, 8), 0),
|
||||||
"RGBA": ("RGBA", II, 2, 1, (8,8,8,8), 2),
|
"RGBA": ("RGBA", II, 2, 1, (8, 8, 8, 8), 2),
|
||||||
"CMYK": ("CMYK", II, 5, 1, (8,8,8,8), None),
|
"CMYK": ("CMYK", II, 5, 1, (8, 8, 8, 8), None),
|
||||||
"YCbCr": ("YCbCr", II, 6, 1, (8,8,8), None),
|
"YCbCr": ("YCbCr", II, 6, 1, (8, 8, 8), None),
|
||||||
"LAB": ("LAB", II, 8, 1, (8,8,8), None),
|
"LAB": ("LAB", II, 8, 1, (8, 8, 8), None),
|
||||||
|
|
||||||
"I;32BS": ("I;32BS", MM, 1, 2, (32,), None),
|
"I;32BS": ("I;32BS", MM, 1, 2, (32,), None),
|
||||||
"I;16B": ("I;16B", MM, 1, 1, (16,), 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),
|
"F;32BF": ("F;32BF", MM, 1, 3, (32,), None),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def _cvt_res(value):
|
def _cvt_res(value):
|
||||||
# convert value to TIFF rational number -- (numerator, denominator)
|
# convert value to TIFF rational number -- (numerator, denominator)
|
||||||
if isinstance(value, collections.Sequence):
|
if isinstance(value, collections.Sequence):
|
||||||
|
@ -973,6 +1008,7 @@ def _cvt_res(value):
|
||||||
value = float(value)
|
value = float(value)
|
||||||
return (int(value * 65536), 65536)
|
return (int(value * 65536), 65536)
|
||||||
|
|
||||||
|
|
||||||
def _save(im, fp, filename):
|
def _save(im, fp, filename):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -982,7 +1018,8 @@ def _save(im, fp, filename):
|
||||||
|
|
||||||
ifd = ImageFileDirectory(prefix)
|
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'
|
libtiff = WRITE_LIBTIFF or compression != 'raw'
|
||||||
|
|
||||||
|
@ -999,17 +1036,16 @@ def _save(im, fp, filename):
|
||||||
ifd[IMAGELENGTH] = im.size[1]
|
ifd[IMAGELENGTH] = im.size[1]
|
||||||
|
|
||||||
# write any arbitrary tags passed in as an ImageFileDirectory
|
# write any arbitrary tags passed in as an ImageFileDirectory
|
||||||
info = im.encoderinfo.get("tiffinfo",{})
|
info = im.encoderinfo.get("tiffinfo", {})
|
||||||
if Image.DEBUG:
|
if Image.DEBUG:
|
||||||
print ("Tiffinfo Keys: %s"% info.keys)
|
print("Tiffinfo Keys: %s" % info.keys)
|
||||||
keys = list(info.keys())
|
keys = list(info.keys())
|
||||||
for key in keys:
|
for key in keys:
|
||||||
ifd[key] = info.get(key)
|
ifd[key] = info.get(key)
|
||||||
try:
|
try:
|
||||||
ifd.tagtype[key] = info.tagtype[key]
|
ifd.tagtype[key] = info.tagtype[key]
|
||||||
except:
|
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
|
# additions written by Greg Couch, gregc@cgl.ucsf.edu
|
||||||
# inspired by image-sig posting from Kevin Cazabon, kcazabon@home.com
|
# 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"]
|
ifd[IMAGEDESCRIPTION] = im.encoderinfo["description"]
|
||||||
if "resolution" in im.encoderinfo:
|
if "resolution" in im.encoderinfo:
|
||||||
ifd[X_RESOLUTION] = ifd[Y_RESOLUTION] \
|
ifd[X_RESOLUTION] = ifd[Y_RESOLUTION] \
|
||||||
= _cvt_res(im.encoderinfo["resolution"])
|
= _cvt_res(im.encoderinfo["resolution"])
|
||||||
if "x resolution" in im.encoderinfo:
|
if "x resolution" in im.encoderinfo:
|
||||||
ifd[X_RESOLUTION] = _cvt_res(im.encoderinfo["x resolution"])
|
ifd[X_RESOLUTION] = _cvt_res(im.encoderinfo["x resolution"])
|
||||||
if "y resolution" in im.encoderinfo:
|
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)
|
stride = len(bits) * ((im.size[0]*bits[0]+7)//8)
|
||||||
ifd[ROWSPERSTRIP] = im.size[1]
|
ifd[ROWSPERSTRIP] = im.size[1]
|
||||||
ifd[STRIPBYTECOUNTS] = stride * im.size[1]
|
ifd[STRIPBYTECOUNTS] = stride * im.size[1]
|
||||||
ifd[STRIPOFFSETS] = 0 # this is adjusted by IFD writer
|
ifd[STRIPOFFSETS] = 0 # this is adjusted by IFD writer
|
||||||
ifd[COMPRESSION] = COMPRESSION_INFO_REV.get(compression,1) # no compression by default
|
# no compression by default:
|
||||||
|
ifd[COMPRESSION] = COMPRESSION_INFO_REV.get(compression, 1)
|
||||||
|
|
||||||
if libtiff:
|
if libtiff:
|
||||||
if Image.DEBUG:
|
if Image.DEBUG:
|
||||||
|
@ -1089,23 +1126,27 @@ def _save(im, fp, filename):
|
||||||
fp.seek(0)
|
fp.seek(0)
|
||||||
_fp = os.dup(fp.fileno())
|
_fp = os.dup(fp.fileno())
|
||||||
|
|
||||||
blocklist = [STRIPOFFSETS, STRIPBYTECOUNTS, ROWSPERSTRIP, ICCPROFILE] # ICC Profile crashes.
|
# ICC Profile crashes.
|
||||||
atts={}
|
blocklist = [STRIPOFFSETS, STRIPBYTECOUNTS, ROWSPERSTRIP, ICCPROFILE]
|
||||||
|
atts = {}
|
||||||
# bits per sample is a single short in the tiff directory, not a list.
|
# bits per sample is a single short in the tiff directory, not a list.
|
||||||
atts[BITSPERSAMPLE] = bits[0]
|
atts[BITSPERSAMPLE] = bits[0]
|
||||||
# Merge the ones that we have with (optional) more bits from
|
# Merge the ones that we have with (optional) more bits from
|
||||||
# the original file, e.g x,y resolution so that we can
|
# the original file, e.g x,y resolution so that we can
|
||||||
# save(load('')) == original file.
|
# 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 k not in atts and k not in blocklist:
|
||||||
if type(v[0]) == tuple and len(v) > 1:
|
if type(v[0]) == tuple and len(v) > 1:
|
||||||
# A tuple of more than one rational tuples
|
# 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]
|
atts[k] = [float(elt[0])/float(elt[1]) for elt in v]
|
||||||
continue
|
continue
|
||||||
if type(v[0]) == tuple and len(v) == 1:
|
if type(v[0]) == tuple and len(v) == 1:
|
||||||
# A tuple of one rational tuples
|
# 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])
|
atts[k] = float(v[0][0])/float(v[0][1])
|
||||||
continue
|
continue
|
||||||
if type(v) == tuple and len(v) > 2:
|
if type(v) == tuple and len(v) > 2:
|
||||||
|
@ -1115,7 +1156,8 @@ def _save(im, fp, filename):
|
||||||
continue
|
continue
|
||||||
if type(v) == tuple and len(v) == 2:
|
if type(v) == tuple and len(v) == 2:
|
||||||
# one rational tuple
|
# 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])
|
atts[k] = float(v[0])/float(v[1])
|
||||||
continue
|
continue
|
||||||
if type(v) == tuple and len(v) == 1:
|
if type(v) == tuple and len(v) == 1:
|
||||||
|
@ -1141,9 +1183,10 @@ def _save(im, fp, filename):
|
||||||
a = (rawmode, compression, _fp, filename, atts)
|
a = (rawmode, compression, _fp, filename, atts)
|
||||||
# print (im.mode, compression, a, im.encoderconfig)
|
# print (im.mode, compression, a, im.encoderconfig)
|
||||||
e = Image._getencoder(im.mode, 'libtiff', 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:
|
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:
|
if not _fp:
|
||||||
fp.write(d)
|
fp.write(d)
|
||||||
if s:
|
if s:
|
||||||
|
@ -1155,13 +1198,12 @@ def _save(im, fp, filename):
|
||||||
offset = ifd.save(fp)
|
offset = ifd.save(fp)
|
||||||
|
|
||||||
ImageFile._save(im, 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 --
|
# -- helper for multi-page save --
|
||||||
if "_debug_multipage" in im.encoderinfo:
|
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
|
im._debug_multipage = ifd
|
||||||
|
|
||||||
#
|
#
|
||||||
|
|
108
PIL/TiffTags.py
|
@ -46,8 +46,8 @@ TAGS = {
|
||||||
(262, 5): "CMYK",
|
(262, 5): "CMYK",
|
||||||
(262, 6): "YCbCr",
|
(262, 6): "YCbCr",
|
||||||
(262, 8): "CieLAB",
|
(262, 8): "CieLAB",
|
||||||
(262, 32803): "CFA", # TIFF/EP, Adobe DNG
|
(262, 32803): "CFA", # TIFF/EP, Adobe DNG
|
||||||
(262, 32892): "LinearRaw", # Adobe DNG
|
(262, 32892): "LinearRaw", # Adobe DNG
|
||||||
|
|
||||||
263: "Thresholding",
|
263: "Thresholding",
|
||||||
264: "CellWidth",
|
264: "CellWidth",
|
||||||
|
@ -147,6 +147,100 @@ TAGS = {
|
||||||
# ICC Profile
|
# ICC Profile
|
||||||
34675: "ICCProfile",
|
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
|
# Adobe DNG
|
||||||
50706: "DNGVersion",
|
50706: "DNGVersion",
|
||||||
50707: "DNGBackwardVersion",
|
50707: "DNGBackwardVersion",
|
||||||
|
@ -161,7 +255,6 @@ TAGS = {
|
||||||
50716: "BlackLevelDeltaV",
|
50716: "BlackLevelDeltaV",
|
||||||
50717: "WhiteLevel",
|
50717: "WhiteLevel",
|
||||||
50718: "DefaultScale",
|
50718: "DefaultScale",
|
||||||
50741: "BestQualityScale", # FIXME! Dictionary contains duplicate keys 50741
|
|
||||||
50719: "DefaultCropOrigin",
|
50719: "DefaultCropOrigin",
|
||||||
50720: "DefaultCropSize",
|
50720: "DefaultCropSize",
|
||||||
50778: "CalibrationIlluminant1",
|
50778: "CalibrationIlluminant1",
|
||||||
|
@ -185,11 +278,12 @@ TAGS = {
|
||||||
50737: "ChromaBlurRadius",
|
50737: "ChromaBlurRadius",
|
||||||
50738: "AntiAliasStrength",
|
50738: "AntiAliasStrength",
|
||||||
50740: "DNGPrivateData",
|
50740: "DNGPrivateData",
|
||||||
50741: "MakerNoteSafety", # FIXME! Dictionary contains duplicate keys 50741
|
50741: "MakerNoteSafety",
|
||||||
|
50780: "BestQualityScale",
|
||||||
|
|
||||||
#ImageJ
|
# ImageJ
|
||||||
50838: "ImageJMetaDataByteCounts", # private tag registered with Adobe
|
50838: "ImageJMetaDataByteCounts", # private tag registered with Adobe
|
||||||
50839: "ImageJMetaData", # private tag registered with Adobe
|
50839: "ImageJMetaData", # private tag registered with Adobe
|
||||||
}
|
}
|
||||||
|
|
||||||
##
|
##
|
||||||
|
|
|
@ -29,6 +29,7 @@ xpm_head = re.compile(b"\"([0-9]*) ([0-9]*) ([0-9]*) ([0-9]*)")
|
||||||
def _accept(prefix):
|
def _accept(prefix):
|
||||||
return prefix[:9] == b"/* XPM */"
|
return prefix[:9] == b"/* XPM */"
|
||||||
|
|
||||||
|
|
||||||
##
|
##
|
||||||
# Image plugin for X11 pixel maps.
|
# Image plugin for X11 pixel maps.
|
||||||
|
|
||||||
|
@ -86,9 +87,9 @@ class XpmImageFile(ImageFile.ImageFile):
|
||||||
elif rgb[0:1] == b"#":
|
elif rgb[0:1] == b"#":
|
||||||
# FIXME: handle colour names (see ImagePalette.py)
|
# FIXME: handle colour names (see ImagePalette.py)
|
||||||
rgb = int(rgb[1:], 16)
|
rgb = int(rgb[1:], 16)
|
||||||
palette[c] = o8((rgb >> 16) & 255) +\
|
palette[c] = (o8((rgb >> 16) & 255) +
|
||||||
o8((rgb >> 8) & 255) +\
|
o8((rgb >> 8) & 255) +
|
||||||
o8(rgb & 255)
|
o8(rgb & 255))
|
||||||
else:
|
else:
|
||||||
# unknown colour
|
# unknown colour
|
||||||
raise ValueError("cannot read this XPM file")
|
raise ValueError("cannot read this XPM file")
|
||||||
|
|
|
@ -12,7 +12,7 @@
|
||||||
# ;-)
|
# ;-)
|
||||||
|
|
||||||
VERSION = '1.1.7' # PIL version
|
VERSION = '1.1.7' # PIL version
|
||||||
PILLOW_VERSION = '2.5.0' # Pillow
|
PILLOW_VERSION = '2.5.3' # Pillow
|
||||||
|
|
||||||
_plugins = ['BmpImagePlugin',
|
_plugins = ['BmpImagePlugin',
|
||||||
'BufrStubImagePlugin',
|
'BufrStubImagePlugin',
|
||||||
|
@ -36,6 +36,7 @@ _plugins = ['BmpImagePlugin',
|
||||||
'McIdasImagePlugin',
|
'McIdasImagePlugin',
|
||||||
'MicImagePlugin',
|
'MicImagePlugin',
|
||||||
'MpegImagePlugin',
|
'MpegImagePlugin',
|
||||||
|
'MpoImagePlugin',
|
||||||
'MspImagePlugin',
|
'MspImagePlugin',
|
||||||
'PalmImagePlugin',
|
'PalmImagePlugin',
|
||||||
'PcdImagePlugin',
|
'PcdImagePlugin',
|
||||||
|
|
|
@ -3,20 +3,25 @@ import os
|
||||||
if bytes is str:
|
if bytes is str:
|
||||||
def isStringType(t):
|
def isStringType(t):
|
||||||
return isinstance(t, basestring)
|
return isinstance(t, basestring)
|
||||||
|
|
||||||
def isPath(f):
|
def isPath(f):
|
||||||
return isinstance(f, basestring)
|
return isinstance(f, basestring)
|
||||||
else:
|
else:
|
||||||
def isStringType(t):
|
def isStringType(t):
|
||||||
return isinstance(t, str)
|
return isinstance(t, str)
|
||||||
|
|
||||||
def isPath(f):
|
def isPath(f):
|
||||||
return isinstance(f, (bytes, str))
|
return isinstance(f, (bytes, str))
|
||||||
|
|
||||||
|
|
||||||
# Checks if an object is a string, and that it points to a directory.
|
# Checks if an object is a string, and that it points to a directory.
|
||||||
def isDirectory(f):
|
def isDirectory(f):
|
||||||
return isPath(f) and os.path.isdir(f)
|
return isPath(f) and os.path.isdir(f)
|
||||||
|
|
||||||
|
|
||||||
class deferred_error(object):
|
class deferred_error(object):
|
||||||
def __init__(self, ex):
|
def __init__(self, ex):
|
||||||
self.ex = ex
|
self.ex = ex
|
||||||
|
|
||||||
def __getattr__(self, elt):
|
def __getattr__(self, elt):
|
||||||
raise self.ex
|
raise self.ex
|
||||||
|
|
|
@ -3,7 +3,7 @@ Pillow
|
||||||
|
|
||||||
*Python Imaging Library (Fork)*
|
*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 <http://pillow.readthedocs.org/>`_.
|
Pillow is the "friendly" PIL fork by `Alex Clark and Contributors <https://github.com/python-pillow/Pillow/graphs/contributors>`_. PIL is the Python Imaging Library by Fredrik Lundh and Contributors. For more information, please `read the documentation <http://pillow.readthedocs.org/>`_, `check the changelog <https://github.com/python-pillow/Pillow/blob/master/CHANGES.rst>`_ and `find out how to contribute <https://github.com/python-pillow/Pillow/blob/master/CONTRIBUTING.md>`_.
|
||||||
|
|
||||||
.. image:: https://travis-ci.org/python-pillow/Pillow.svg?branch=master
|
.. image:: https://travis-ci.org/python-pillow/Pillow.svg?branch=master
|
||||||
:target: https://travis-ci.org/python-pillow/Pillow
|
: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
|
.. image:: https://coveralls.io/repos/python-pillow/Pillow/badge.png?branch=master
|
||||||
:target: https://coveralls.io/r/python-pillow/Pillow?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
|
||||||
|
|
16
Scripts/createfontdatachunk.py
Normal file
|
@ -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
|
10
Tests/32bit_segfault_check.py
Normal file
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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``.
|
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
|
Execution
|
||||||
---------
|
---------
|
||||||
|
|
||||||
Run the tests from the root of the Pillow source distribution::
|
**If Pillow has been built in-place**
|
||||||
|
|
||||||
python selftest.py
|
|
||||||
nosetests Tests/test_*.py
|
|
||||||
|
|
||||||
Or with coverage::
|
|
||||||
|
|
||||||
coverage run --append --include=PIL/* selftest.py
|
|
||||||
coverage run --append --include=PIL/* -m nose Tests/test_*.py
|
|
||||||
coverage report
|
|
||||||
coverage html
|
|
||||||
open htmlcov/index.html
|
|
||||||
|
|
||||||
To run an individual test::
|
To run an individual test::
|
||||||
|
|
||||||
python Tests/test_image.py
|
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
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
10
Tests/check_icns_dos.py
Normal file
|
@ -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')))
|
11
Tests/check_j2k_dos.py
Normal file
|
@ -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')))
|
||||||
|
|
42
Tests/check_j2k_leaks.py
Executable file
|
@ -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()
|
|
@ -5,22 +5,23 @@ from __future__ import print_function
|
||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
import os
|
import os
|
||||||
import glob
|
|
||||||
|
|
||||||
if sys.version_info[:2] <= (2, 6):
|
if sys.version_info[:2] <= (2, 6):
|
||||||
import unittest2 as unittest
|
import unittest2 as unittest
|
||||||
else:
|
else:
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
def tearDownModule():
|
|
||||||
#remove me later
|
|
||||||
pass
|
|
||||||
|
|
||||||
class PillowTestCase(unittest.TestCase):
|
class PillowTestCase(unittest.TestCase):
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
unittest.TestCase.__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):
|
def run(self, result=None):
|
||||||
self.currentResult = result # remember result for use later
|
self.currentResult = result # remember result for use later
|
||||||
|
@ -40,7 +41,7 @@ class PillowTestCase(unittest.TestCase):
|
||||||
except OSError:
|
except OSError:
|
||||||
pass # report?
|
pass # report?
|
||||||
else:
|
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):
|
def assert_almost_equal(self, a, b, msg=None, eps=1e-6):
|
||||||
self.assertLess(
|
self.assertLess(
|
||||||
|
@ -99,7 +100,7 @@ class PillowTestCase(unittest.TestCase):
|
||||||
ave_diff = float(diff)/(a.size[0]*a.size[1])
|
ave_diff = float(diff)/(a.size[0]*a.size[1])
|
||||||
self.assertGreaterEqual(
|
self.assertGreaterEqual(
|
||||||
epsilon, ave_diff,
|
epsilon, ave_diff,
|
||||||
msg or "average pixel value difference %.4f > epsilon %.4f" % (
|
(msg or '') + " average pixel value difference %.4f > epsilon %.4f" % (
|
||||||
ave_diff, epsilon))
|
ave_diff, epsilon))
|
||||||
|
|
||||||
def assert_warning(self, warn_class, func):
|
def assert_warning(self, warn_class, func):
|
||||||
|
@ -123,7 +124,8 @@ class PillowTestCase(unittest.TestCase):
|
||||||
self.assertTrue(found)
|
self.assertTrue(found)
|
||||||
return result
|
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
|
# Skip if platform/travis matches, and
|
||||||
# PILLOW_RUN_KNOWN_BAD is not true in the environment.
|
# PILLOW_RUN_KNOWN_BAD is not true in the environment.
|
||||||
if bool(os.environ.get('PILLOW_RUN_KNOWN_BAD', False)):
|
if bool(os.environ.get('PILLOW_RUN_KNOWN_BAD', False)):
|
||||||
|
@ -134,7 +136,9 @@ class PillowTestCase(unittest.TestCase):
|
||||||
if platform is not None:
|
if platform is not None:
|
||||||
skip = sys.platform.startswith(platform)
|
skip = sys.platform.startswith(platform)
|
||||||
if travis is not None:
|
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:
|
if skip:
|
||||||
self.skipTest(msg or "Known Bad Test")
|
self.skipTest(msg or "Known Bad Test")
|
||||||
|
|
||||||
|
@ -142,8 +146,8 @@ class PillowTestCase(unittest.TestCase):
|
||||||
assert template[:5] in ("temp.", "temp_")
|
assert template[:5] in ("temp.", "temp_")
|
||||||
(fd, path) = tempfile.mkstemp(template[4:], template[:4])
|
(fd, path) = tempfile.mkstemp(template[4:], template[:4])
|
||||||
os.close(fd)
|
os.close(fd)
|
||||||
|
|
||||||
self.addCleanup(self.delete_tempfile, path)
|
self.addCleanup(self.delete_tempfile, path)
|
||||||
return path
|
return path
|
||||||
|
|
||||||
def open_withImagemagick(self, f):
|
def open_withImagemagick(self, f):
|
||||||
|
@ -155,8 +159,8 @@ class PillowTestCase(unittest.TestCase):
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
return Image.open(outfile)
|
return Image.open(outfile)
|
||||||
raise IOError()
|
raise IOError()
|
||||||
|
|
||||||
|
|
||||||
# helpers
|
# helpers
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
|
@ -176,6 +180,26 @@ def tostring(im, format, **options):
|
||||||
return out.getvalue()
|
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={}):
|
def lena(mode="RGB", cache={}):
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
im = None
|
im = None
|
||||||
|
@ -210,17 +234,21 @@ def command_succeeds(cmd):
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def djpeg_available():
|
def djpeg_available():
|
||||||
return command_succeeds(['djpeg', '--help'])
|
return command_succeeds(['djpeg', '--help'])
|
||||||
|
|
||||||
|
|
||||||
def cjpeg_available():
|
def cjpeg_available():
|
||||||
return command_succeeds(['cjpeg', '--help'])
|
return command_succeeds(['cjpeg', '--help'])
|
||||||
|
|
||||||
|
|
||||||
def netpbm_available():
|
def netpbm_available():
|
||||||
return command_succeeds(["ppmquant", "--help"]) and \
|
return (command_succeeds(["ppmquant", "--help"]) and
|
||||||
command_succeeds(["ppmtogif", "--help"])
|
command_succeeds(["ppmtogif", "--help"]))
|
||||||
|
|
||||||
|
|
||||||
def imagemagick_available():
|
def imagemagick_available():
|
||||||
return command_succeeds(['convert', '-version'])
|
return command_succeeds(['convert', '-version'])
|
||||||
|
|
||||||
# End of file
|
# End of file
|
||||||
|
|
BIN
Tests/images/deerstalker.cur
Normal file
After Width: | Height: | Size: 4.2 KiB |
BIN
Tests/images/default_font.png
Normal file
After Width: | Height: | Size: 586 B |
BIN
Tests/images/dispose_bgnd.gif
Normal file
After Width: | Height: | Size: 1.4 KiB |
BIN
Tests/images/dispose_none.gif
Normal file
After Width: | Height: | Size: 1.4 KiB |
BIN
Tests/images/dispose_prev.gif
Normal file
After Width: | Height: | Size: 1.4 KiB |
BIN
Tests/images/effect_mandelbrot.png
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
Tests/images/effect_spread.png
Normal file
After Width: | Height: | Size: 40 KiB |
BIN
Tests/images/frozenpond.mpo
Normal file
After Width: | Height: | Size: 162 KiB |
6
Tests/images/gimp_gradient.ggr
Normal file
|
@ -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
|
7
Tests/images/gimp_gradient_with_name.ggr
Normal file
|
@ -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
|
BIN
Tests/images/hopper.bw
Normal file
BIN
Tests/images/hopper.dcx
Normal file
BIN
Tests/images/hopper.gif
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
Tests/images/hopper.ico
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
Tests/images/hopper.jpg
Normal file
After Width: | Height: | Size: 6.3 KiB |
BIN
Tests/images/hopper.png
Normal file
After Width: | Height: | Size: 30 KiB |
BIN
Tests/images/hopper.ppm
Normal file
BIN
Tests/images/hopper.ras
Normal file
BIN
Tests/images/hopper.rgb
Normal file
BIN
Tests/images/hopper.spider
Normal file
BIN
Tests/images/hopper.tar
Normal file
BIN
Tests/images/iptc.jpg
Normal file
After Width: | Height: | Size: 20 KiB |
BIN
Tests/images/iss634.gif
Normal file
After Width: | Height: | Size: 271 KiB |
Before Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 37 KiB |
BIN
Tests/images/lena_gray.jpg
Normal file
After Width: | Height: | Size: 22 KiB |
BIN
Tests/images/multipage-lastframe.tif
Normal file
BIN
Tests/images/multipage.tiff
Normal file
BIN
Tests/images/rectangle_surrounding_text.png
Normal file
After Width: | Height: | Size: 1.5 KiB |
BIN
Tests/images/sugarshack.mpo
Normal file
After Width: | Height: | Size: 117 KiB |
BIN
Tests/images/tga_id_field.tga
Normal file
BIN
Tests/images/transparent.sgi
Normal file
|
@ -9,6 +9,7 @@ modes = [
|
||||||
"RGB", "RGBA", "RGBa", "RGBX",
|
"RGB", "RGBA", "RGBa", "RGBX",
|
||||||
"CMYK",
|
"CMYK",
|
||||||
"YCbCr",
|
"YCbCr",
|
||||||
|
"LAB", "HSV",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
from helper import unittest, PillowTestCase, tearDownModule
|
from helper import unittest, PillowTestCase
|
||||||
|
|
||||||
import PIL
|
import PIL
|
||||||
import PIL.Image
|
import PIL.Image
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
from helper import unittest, PillowTestCase, tearDownModule
|
from helper import unittest, PillowTestCase
|
||||||
|
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
import os
|
import os
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
from helper import unittest, PillowTestCase, tearDownModule, lena
|
from helper import unittest, PillowTestCase, lena
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import cffi
|
import cffi
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
from helper import unittest, PillowTestCase, tearDownModule
|
from helper import unittest, PillowTestCase
|
||||||
|
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
from helper import unittest, PillowTestCase, tearDownModule, lena
|
from helper import unittest, PillowTestCase, lena
|
||||||
|
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
import io
|
import io
|
||||||
|
|
27
Tests/test_file_cur.py
Normal file
|
@ -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
|
45
Tests/test_file_dcx.py
Normal file
|
@ -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
|
|
@ -1,4 +1,4 @@
|
||||||
from helper import unittest, PillowTestCase, tearDownModule
|
from helper import unittest, PillowTestCase
|
||||||
|
|
||||||
from PIL import Image, EpsImagePlugin
|
from PIL import Image, EpsImagePlugin
|
||||||
import io
|
import io
|
||||||
|
@ -63,6 +63,17 @@ class TestFileEps(PillowTestCase):
|
||||||
with io.open(self.tempfile('temp_iobase.eps'), 'wb') as fh:
|
with io.open(self.tempfile('temp_iobase.eps'), 'wb') as fh:
|
||||||
image1.save(fh, 'EPS')
|
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):
|
def test_render_scale1(self):
|
||||||
# We need png support for these render test
|
# We need png support for these render test
|
||||||
codecs = dir(Image.core)
|
codecs = dir(Image.core)
|
||||||
|
@ -137,6 +148,83 @@ class TestFileEps(PillowTestCase):
|
||||||
# open image with binary preview
|
# open image with binary preview
|
||||||
Image.open(file3)
|
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__':
|
if __name__ == '__main__':
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
from helper import unittest, PillowTestCase, tearDownModule
|
from helper import unittest, PillowTestCase
|
||||||
|
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
|
||||||
|
|
|
@ -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 Image
|
||||||
from PIL import GifImagePlugin
|
from PIL import GifImagePlugin
|
||||||
|
@ -6,8 +6,9 @@ from PIL import GifImagePlugin
|
||||||
codecs = dir(Image.core)
|
codecs = dir(Image.core)
|
||||||
|
|
||||||
# sample gif stream
|
# sample gif stream
|
||||||
file = "Tests/images/lena.gif"
|
TEST_GIF = "Tests/images/hopper.gif"
|
||||||
with open(file, "rb") as f:
|
|
||||||
|
with open(TEST_GIF, "rb") as f:
|
||||||
data = f.read()
|
data = f.read()
|
||||||
|
|
||||||
|
|
||||||
|
@ -18,7 +19,7 @@ class TestFileGif(PillowTestCase):
|
||||||
self.skipTest("gif support not available") # can this happen?
|
self.skipTest("gif support not available") # can this happen?
|
||||||
|
|
||||||
def test_sanity(self):
|
def test_sanity(self):
|
||||||
im = Image.open(file)
|
im = Image.open(TEST_GIF)
|
||||||
im.load()
|
im.load()
|
||||||
self.assertEqual(im.mode, "P")
|
self.assertEqual(im.mode, "P")
|
||||||
self.assertEqual(im.size, (128, 128))
|
self.assertEqual(im.size, (128, 128))
|
||||||
|
@ -35,9 +36,17 @@ class TestFileGif(PillowTestCase):
|
||||||
self.assertEqual(test(0), 800)
|
self.assertEqual(test(0), 800)
|
||||||
self.assertEqual(test(1), 38)
|
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):
|
def test_roundtrip(self):
|
||||||
out = self.tempfile('temp.gif')
|
out = self.tempfile('temp.gif')
|
||||||
im = lena()
|
im = hopper()
|
||||||
im.save(out)
|
im.save(out)
|
||||||
reread = Image.open(out)
|
reread = Image.open(out)
|
||||||
|
|
||||||
|
@ -46,17 +55,17 @@ class TestFileGif(PillowTestCase):
|
||||||
def test_roundtrip2(self):
|
def test_roundtrip2(self):
|
||||||
# see https://github.com/python-pillow/Pillow/issues/403
|
# see https://github.com/python-pillow/Pillow/issues/403
|
||||||
out = self.tempfile('temp.gif')
|
out = self.tempfile('temp.gif')
|
||||||
im = Image.open('Tests/images/lena.gif')
|
im = Image.open(TEST_GIF)
|
||||||
im2 = im.copy()
|
im2 = im.copy()
|
||||||
im2.save(out)
|
im2.save(out)
|
||||||
reread = Image.open(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):
|
def test_palette_handling(self):
|
||||||
# see https://github.com/python-pillow/Pillow/issues/513
|
# 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.convert('RGB')
|
||||||
|
|
||||||
im = im.resize((100, 100), Image.ANTIALIAS)
|
im = im.resize((100, 100), Image.ANTIALIAS)
|
||||||
|
@ -92,7 +101,7 @@ class TestFileGif(PillowTestCase):
|
||||||
|
|
||||||
@unittest.skipUnless(netpbm_available(), "netpbm not available")
|
@unittest.skipUnless(netpbm_available(), "netpbm not available")
|
||||||
def test_save_netpbm_bmp_mode(self):
|
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")
|
tempfile = self.tempfile("temp.gif")
|
||||||
GifImagePlugin._save_netpbm(img, 0, tempfile)
|
GifImagePlugin._save_netpbm(img, 0, tempfile)
|
||||||
|
@ -100,12 +109,56 @@ class TestFileGif(PillowTestCase):
|
||||||
|
|
||||||
@unittest.skipUnless(netpbm_available(), "netpbm not available")
|
@unittest.skipUnless(netpbm_available(), "netpbm not available")
|
||||||
def test_save_netpbm_l_mode(self):
|
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")
|
tempfile = self.tempfile("temp.gif")
|
||||||
GifImagePlugin._save_netpbm(img, 0, tempfile)
|
GifImagePlugin._save_netpbm(img, 0, tempfile)
|
||||||
self.assert_image_similar(img, Image.open(tempfile).convert("L"), 0)
|
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__':
|
if __name__ == '__main__':
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|
127
Tests/test_file_gimpgradient.py
Normal file
|
@ -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
|
|
@ -1,4 +1,4 @@
|
||||||
from helper import unittest, PillowTestCase, tearDownModule
|
from helper import unittest, PillowTestCase
|
||||||
|
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
|
||||||
|
|
|
@ -1,16 +1,16 @@
|
||||||
from helper import unittest, PillowTestCase, tearDownModule
|
from helper import unittest, PillowTestCase
|
||||||
|
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
|
||||||
# sample ppm stream
|
# sample ppm stream
|
||||||
file = "Tests/images/lena.ico"
|
TEST_ICO_FILE = "Tests/images/hopper.ico"
|
||||||
data = open(file, "rb").read()
|
TEST_DATA = open(TEST_ICO_FILE, "rb").read()
|
||||||
|
|
||||||
|
|
||||||
class TestFileIco(PillowTestCase):
|
class TestFileIco(PillowTestCase):
|
||||||
|
|
||||||
def test_sanity(self):
|
def test_sanity(self):
|
||||||
im = Image.open(file)
|
im = Image.open(TEST_ICO_FILE)
|
||||||
im.load()
|
im.load()
|
||||||
self.assertEqual(im.mode, "RGBA")
|
self.assertEqual(im.mode, "RGBA")
|
||||||
self.assertEqual(im.size, (16, 16))
|
self.assertEqual(im.size, (16, 16))
|
||||||
|
|
79
Tests/test_file_iptc.py
Normal file
|
@ -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
|
|
@ -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
|
from helper import djpeg_available, cjpeg_available
|
||||||
|
|
||||||
import random
|
import random
|
||||||
|
@ -60,7 +60,8 @@ class TestFileJpeg(PillowTestCase):
|
||||||
self.assertGreater(y, 0.8)
|
self.assertGreater(y, 0.8)
|
||||||
self.assertEqual(k, 0.0)
|
self.assertEqual(k, 0.0)
|
||||||
# the opposite corner is black
|
# 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)
|
self.assertGreater(k, 0.9)
|
||||||
# roundtrip, and check again
|
# roundtrip, and check again
|
||||||
im = self.roundtrip(im)
|
im = self.roundtrip(im)
|
||||||
|
@ -69,7 +70,8 @@ class TestFileJpeg(PillowTestCase):
|
||||||
self.assertGreater(m, 0.8)
|
self.assertGreater(m, 0.8)
|
||||||
self.assertGreater(y, 0.8)
|
self.assertGreater(y, 0.8)
|
||||||
self.assertEqual(k, 0.0)
|
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)
|
self.assertGreater(k, 0.9)
|
||||||
|
|
||||||
def test_dpi(self):
|
def test_dpi(self):
|
||||||
|
@ -150,7 +152,8 @@ class TestFileJpeg(PillowTestCase):
|
||||||
if py3:
|
if py3:
|
||||||
a = bytes(random.randint(0, 255) for _ in range(256 * 256 * 3))
|
a = bytes(random.randint(0, 255) for _ in range(256 * 256 * 3))
|
||||||
else:
|
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)
|
im = Image.frombuffer("RGB", (256, 256), a, "raw", "RGB", 0, 1)
|
||||||
# this requires more bytes than pixels in the image
|
# this requires more bytes than pixels in the image
|
||||||
im.save(f, format="JPEG", progressive=True, quality=100)
|
im.save(f, format="JPEG", progressive=True, quality=100)
|
||||||
|
@ -216,10 +219,23 @@ class TestFileJpeg(PillowTestCase):
|
||||||
info = im._getexif()
|
info = im._getexif()
|
||||||
self.assertEqual(info[305], 'Adobe Photoshop CS Macintosh')
|
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):
|
def test_quality_keep(self):
|
||||||
|
# RGB
|
||||||
im = Image.open("Tests/images/lena.jpg")
|
im = Image.open("Tests/images/lena.jpg")
|
||||||
f = self.tempfile('temp.jpg')
|
f = self.tempfile('temp.jpg')
|
||||||
im.save(f, quality='keep')
|
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):
|
def test_junk_jpeg_header(self):
|
||||||
# https://github.com/python-pillow/Pillow/issues/630
|
# https://github.com/python-pillow/Pillow/issues/630
|
||||||
|
@ -231,11 +247,13 @@ class TestFileJpeg(PillowTestCase):
|
||||||
qtables = im.quantization
|
qtables = im.quantization
|
||||||
reloaded = self.roundtrip(im, qtables=qtables, subsampling=0)
|
reloaded = self.roundtrip(im, qtables=qtables, subsampling=0)
|
||||||
self.assertEqual(im.quantization, reloaded.quantization)
|
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_low'),
|
||||||
self.assert_image_similar(im, self.roundtrip(im, qtables='web_high'), 30)
|
30)
|
||||||
|
self.assert_image_similar(im, self.roundtrip(im, qtables='web_high'),
|
||||||
|
30)
|
||||||
self.assert_image_similar(im, self.roundtrip(im, qtables='keep'), 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 """
|
standard_l_qtable = [int(s) for s in """
|
||||||
16 11 10 16 24 40 51 61
|
16 11 10 16 24 40 51 61
|
||||||
12 12 14 19 26 58 60 55
|
12 12 14 19 26 58 60 55
|
||||||
|
@ -247,7 +265,7 @@ class TestFileJpeg(PillowTestCase):
|
||||||
72 92 95 98 112 100 103 99
|
72 92 95 98 112 100 103 99
|
||||||
""".split(None)]
|
""".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
|
17 18 24 47 99 99 99 99
|
||||||
18 21 26 66 99 99 99 99
|
18 21 26 66 99 99 99 99
|
||||||
24 26 56 99 99 99 99 99
|
24 26 56 99 99 99 99 99
|
||||||
|
@ -272,8 +290,8 @@ class TestFileJpeg(PillowTestCase):
|
||||||
# dict of qtable lists
|
# dict of qtable lists
|
||||||
self.assert_image_similar(im,
|
self.assert_image_similar(im,
|
||||||
self.roundtrip(im,
|
self.roundtrip(im,
|
||||||
qtables={0:standard_l_qtable,
|
qtables={0: standard_l_qtable,
|
||||||
1:standard_chrominance_qtable}),
|
1: standard_chrominance_qtable}),
|
||||||
30)
|
30)
|
||||||
|
|
||||||
@unittest.skipUnless(djpeg_available(), "djpeg not available")
|
@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
|
# Default save quality is 75%, so a tiny bit of difference is alright
|
||||||
self.assert_image_similar(img, Image.open(tempfile), 1)
|
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__':
|
if __name__ == '__main__':
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
from helper import unittest, PillowTestCase, tearDownModule
|
from helper import unittest, PillowTestCase
|
||||||
|
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
|
@ -52,7 +52,8 @@ class TestFileJpeg2k(PillowTestCase):
|
||||||
def test_lossless(self):
|
def test_lossless(self):
|
||||||
im = Image.open('Tests/images/test-card-lossless.jp2')
|
im = Image.open('Tests/images/test-card-lossless.jp2')
|
||||||
im.load()
|
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)
|
self.assert_image_similar(im, test_card, 1.0e-3)
|
||||||
|
|
||||||
def test_lossy_tiled(self):
|
def test_lossy_tiled(self):
|
||||||
|
|
|
@ -1,11 +1,10 @@
|
||||||
from helper import unittest, PillowTestCase, tearDownModule, lena, py3
|
from helper import unittest, PillowTestCase, lena, py3
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from PIL import Image, TiffImagePlugin
|
from PIL import Image, TiffImagePlugin
|
||||||
|
|
||||||
|
class LibTiffTestCase(PillowTestCase):
|
||||||
class TestFileLibTiff(PillowTestCase):
|
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
codecs = dir(Image.core)
|
codecs = dir(Image.core)
|
||||||
|
@ -32,6 +31,8 @@ class TestFileLibTiff(PillowTestCase):
|
||||||
out = self.tempfile("temp.png")
|
out = self.tempfile("temp.png")
|
||||||
im.save(out)
|
im.save(out)
|
||||||
|
|
||||||
|
class TestFileLibTiff(LibTiffTestCase):
|
||||||
|
|
||||||
def test_g4_tiff(self):
|
def test_g4_tiff(self):
|
||||||
"""Test the ordinary file path load path"""
|
"""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.fstat(fn))
|
||||||
self.assertRaises(OSError, lambda: os.close(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__':
|
if __name__ == '__main__':
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|