Merge with master
|
@ -11,4 +11,4 @@ exclude_lines =
|
|||
if __name__ == .__main__.:
|
||||
# Don't complain about debug code
|
||||
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:
|
||||
- "pypy"
|
||||
- "pypy3"
|
||||
- 2.6
|
||||
- 2.7
|
||||
- "2.7_with_system_site_packages" # For PyQt4
|
||||
- 3.2
|
||||
- 3.3
|
||||
- 3.4
|
||||
|
||||
install:
|
||||
- "sudo apt-get -qq install libfreetype6-dev liblcms2-dev python-qt4 ghostscript libffi-dev libjpeg-turbo-progs cmake imagemagick"
|
||||
- "pip install cffi"
|
||||
- "pip install coveralls nose pyroma"
|
||||
- if [ "$TRAVIS_PYTHON_VERSION" == "2.6" ]; then pip install unittest2; fi
|
||||
- "travis_retry sudo apt-get -qq install libfreetype6-dev liblcms2-dev python-qt4 ghostscript libffi-dev libjpeg-turbo-progs cmake imagemagick"
|
||||
- "travis_retry pip install cffi"
|
||||
- "travis_retry pip install coverage nose"
|
||||
|
||||
# Pyroma installation is slow on Py3, so just do it for Py2.
|
||||
- if [ ${TRAVIS_PYTHON_VERSION:0:1} == "2" ]; then travis_retry pip install pyroma; fi
|
||||
|
||||
- if [ "$TRAVIS_PYTHON_VERSION" == "2.6" ]; then travis_retry pip install unittest2; fi
|
||||
|
||||
# webp
|
||||
- pushd depends && ./install_webp.sh && popd
|
||||
|
@ -28,22 +34,27 @@ install:
|
|||
script:
|
||||
- coverage erase
|
||||
- python setup.py clean
|
||||
- python setup.py build_ext --inplace
|
||||
- CFLAGS="-coverage" python setup.py build_ext --inplace
|
||||
|
||||
# Don't cover PyPy: it fails intermittently and is x5.8 slower (#640)
|
||||
- if [ "$TRAVIS_PYTHON_VERSION" == "pypy" ]; then time python selftest.py; fi
|
||||
- if [ "$TRAVIS_PYTHON_VERSION" == "pypy" ]; then time nosetests Tests/test_*.py; fi
|
||||
|
||||
# Cover the others
|
||||
- if [ "$TRAVIS_PYTHON_VERSION" != "pypy" ]; then time coverage run --append --include=PIL/* selftest.py; fi
|
||||
- if [ "$TRAVIS_PYTHON_VERSION" != "pypy" ]; then time coverage run --append --include=PIL/* -m nose Tests/test_*.py; fi
|
||||
- coverage run --append --include=PIL/* selftest.py
|
||||
- coverage run --append --include=PIL/* -m nose -vx Tests/test_*.py
|
||||
|
||||
after_success:
|
||||
- coverage report
|
||||
# No need to send empty coverage to Coveralls for PyPy
|
||||
- if [ "$TRAVIS_PYTHON_VERSION" != "pypy" ]; then coveralls; fi
|
||||
# gather the coverage data
|
||||
- travis_retry sudo apt-get -qq install lcov
|
||||
- lcov --capture --directory . -b . --output-file coverage.info
|
||||
# filter to remove system headers
|
||||
- lcov --remove coverage.info '/usr/*' -o coverage.filtered.info
|
||||
# convert to json
|
||||
- travis_retry gem install coveralls-lcov
|
||||
- coveralls-lcov -v -n coverage.filtered.info > coverage.c.json
|
||||
|
||||
- pip install pep8 pyflakes
|
||||
- coverage report
|
||||
- travis_retry pip install coveralls-merge
|
||||
- coveralls-merge coverage.c.json
|
||||
|
||||
|
||||
- travis_retry pip install pep8 pyflakes
|
||||
- pep8 --statistics --count PIL/*.py
|
||||
- pep8 --statistics --count Tests/*.py
|
||||
- pyflakes PIL/*.py | tee >(wc -l)
|
||||
|
@ -54,3 +65,5 @@ after_success:
|
|||
# (Installation is very slow on Py3, so just do it for Py2.)
|
||||
- if [ ${TRAVIS_PYTHON_VERSION:0:1} == "2" ]; then Scripts/diffcover-install.sh; fi
|
||||
- if [ ${TRAVIS_PYTHON_VERSION:0:1} == "2" ]; then Scripts/diffcover-run.sh; fi
|
||||
matrix:
|
||||
fast_finish: true
|
||||
|
|
114
CHANGES.rst
|
@ -4,9 +4,115 @@ Changelog (Pillow)
|
|||
2.6.0 (unreleased)
|
||||
------------------
|
||||
|
||||
- Test PalmImagePlugin and method to skip known bad tests
|
||||
- Jpeg2k Decode/Encode Memory Leak Fix #898
|
||||
[joshware, wiredfool]
|
||||
|
||||
- EpsFilePlugin Speed improvements #886
|
||||
[wiredfool, karstenw]
|
||||
|
||||
- Don't resize if already the right size.
|
||||
[radarhere]
|
||||
|
||||
- Fix for reading multipage TIFFs #885
|
||||
[kostrom, wiredfool]
|
||||
|
||||
- Correctly handle saving gray and CMYK JPEGs with quality=keep #857
|
||||
[etienned]
|
||||
|
||||
- Correct duplicate Tiff Metadata and Exif tag values
|
||||
[hugovk]
|
||||
|
||||
- Windows fixes #871
|
||||
[wiredfool]
|
||||
|
||||
- Fix TGA files with image ID field #856
|
||||
[megabuz]
|
||||
|
||||
- Fixed wrong P-mode of small, unoptimized L-mode GIF #843
|
||||
[uvNikita]
|
||||
|
||||
- Fixed CVE-2014-3598, a DOS in the Jpeg2KImagePlugin
|
||||
[Andrew Drake]
|
||||
|
||||
- Fixed CVE-2014-3589, a DOS in the IcnsImagePlugin
|
||||
[Andrew Drake]
|
||||
|
||||
- setup.py: Close open file handle before deleting #844
|
||||
[divergentdave]
|
||||
|
||||
- Return Profile with Transformed Images #837
|
||||
[wiredfool]
|
||||
|
||||
- Changed docstring to refer to the correct function #836
|
||||
[MatMoore]
|
||||
|
||||
- Adding coverage support for C code tests #833
|
||||
[wiredfool]
|
||||
|
||||
- PyPy performance improvements #821
|
||||
[wiredfool]
|
||||
|
||||
- Added support for reading MPO files
|
||||
[Feneric]
|
||||
|
||||
- Added support for encoding and decoding iTXt chunks #818
|
||||
[dolda2000]
|
||||
|
||||
- HSV Support #816
|
||||
[wiredfool]
|
||||
|
||||
- Removed unusable ImagePalette.new()
|
||||
[hugovk]
|
||||
|
||||
- Fix Scrambled XPM #808
|
||||
[wiredfool]
|
||||
|
||||
- Doc cleanup
|
||||
[wiredfool]
|
||||
|
||||
- Fix `ImageStat` docs
|
||||
[akx]
|
||||
|
||||
- Added docs for ExifTags
|
||||
[Wintermute3]
|
||||
|
||||
- More tests for CurImagePlugin, DcxImagePlugin, Effects.c, GimpGradientFile, ImageFont, ImageMath, ImagePalette, IptcImagePlugin, SpiderImagePlugin, SgiImagePlugin, XpmImagePlugin and _util
|
||||
[hugovk]
|
||||
|
||||
- Fix return value of FreeTypeFont.textsize() does not include font offsets
|
||||
[tk0miya]
|
||||
|
||||
- Fix dispose calculations for animated GIFs #765
|
||||
[larsjsol]
|
||||
|
||||
- Added class checking to Image __eq__ function #775
|
||||
[radarhere, hugovk]
|
||||
|
||||
- Test PalmImagePlugin and method to skip known bad tests #776
|
||||
[hugovk, wiredfool]
|
||||
|
||||
2.5.3 (2014-08-18)
|
||||
------------------
|
||||
|
||||
- Fixed CVE-2014-3598, a DOS in the Jpeg2KImagePlugin (backport)
|
||||
[Andrew Drake]
|
||||
|
||||
|
||||
2.5.2 (2014-08-13)
|
||||
------------------
|
||||
|
||||
- Fixed CVE-2014-3589, a DOS in the IcnsImagePlugin (backport)
|
||||
[Andrew Drake]
|
||||
|
||||
|
||||
2.5.1 (2014-07-10)
|
||||
------------------
|
||||
|
||||
- Fixed install issue if Multiprocessing.Pool is not available
|
||||
[wiredfool]
|
||||
|
||||
- 32bit mult overflow fix #782
|
||||
[wiredfool]
|
||||
|
||||
2.5.0 (2014-07-01)
|
||||
------------------
|
||||
|
@ -227,6 +333,12 @@ Changelog (Pillow)
|
|||
- Prefer homebrew freetype over X11 freetype (but still allow both)
|
||||
[dmckeone]
|
||||
|
||||
2.3.2 (2014-08-13)
|
||||
------------------
|
||||
|
||||
- Fixed CVE-2014-3589, a DOS in the IcnsImagePlugin (backport)
|
||||
[Andrew Drake]
|
||||
|
||||
2.3.1 (2014-03-14)
|
||||
------------------
|
||||
|
||||
|
|
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 *.h
|
||||
include *.md
|
||||
include *.py
|
||||
include *.rst
|
||||
include *.txt
|
||||
|
@ -25,14 +27,20 @@ recursive-include Images *.xpm
|
|||
recursive-include PIL *.md
|
||||
recursive-include Sane *.c
|
||||
recursive-include Sane *.py
|
||||
recursive-include Sane *.rst
|
||||
recursive-include Sane *.txt
|
||||
recursive-include Sane CHANGES
|
||||
recursive-include Sane README
|
||||
recursive-include Scripts *.py
|
||||
recursive-include Scripts *.rst
|
||||
recursive-include Scripts *.sh
|
||||
recursive-include Scripts README
|
||||
recursive-include Tests *.bdf
|
||||
recursive-include Tests *.bin
|
||||
recursive-include Tests *.bmp
|
||||
recursive-include Tests *.bw
|
||||
recursive-include Tests *.cur
|
||||
recursive-include Tests *.dcx
|
||||
recursive-include Tests *.doc
|
||||
recursive-include Tests *.eps
|
||||
recursive-include Tests *.fli
|
||||
|
@ -46,6 +54,7 @@ recursive-include Tests *.j2k
|
|||
recursive-include Tests *.jp2
|
||||
recursive-include Tests *.jpg
|
||||
recursive-include Tests *.lut
|
||||
recursive-include Tests *.mpo
|
||||
recursive-include Tests *.pbm
|
||||
recursive-include Tests *.pcf
|
||||
recursive-include Tests *.pcx
|
||||
|
@ -55,7 +64,10 @@ recursive-include Tests *.png
|
|||
recursive-include Tests *.ppm
|
||||
recursive-include Tests *.psd
|
||||
recursive-include Tests *.py
|
||||
recursive-include Tests *.ras
|
||||
recursive-include Tests *.rgb
|
||||
recursive-include Tests *.rst
|
||||
recursive-include Tests *.sgi
|
||||
recursive-include Tests *.spider
|
||||
recursive-include Tests *.tar
|
||||
recursive-include Tests *.tif
|
||||
|
@ -65,22 +77,20 @@ recursive-include Tests *.txt
|
|||
recursive-include Tests *.webp
|
||||
recursive-include Tests *.xpm
|
||||
recursive-include Tk *.c
|
||||
recursive-include Tk *.txt
|
||||
recursive-include Tk *.rst
|
||||
recursive-include depends *.sh
|
||||
recursive-include Tk *.txt
|
||||
recursive-include depends *.rst
|
||||
recursive-include depends *.sh
|
||||
recursive-include docs *.bat
|
||||
recursive-include docs *.gitignore
|
||||
recursive-include docs *.html
|
||||
recursive-include docs *.py
|
||||
recursive-include docs *.rst
|
||||
recursive-include docs *.txt
|
||||
recursive-include docs Guardfile
|
||||
recursive-include docs Makefile
|
||||
recursive-include docs BUILDME
|
||||
recursive-include docs COPYING
|
||||
recursive-include docs Guardfile
|
||||
recursive-include docs LICENSE
|
||||
recursive-include docs Makefile
|
||||
recursive-include libImaging *.c
|
||||
recursive-include libImaging *.h
|
||||
recursive-include Sane *.rst
|
||||
recursive-include Scripts *.rst
|
||||
|
|
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:
|
||||
virtualenv .
|
||||
|
@ -15,13 +27,14 @@ pre:
|
|||
clean:
|
||||
python setup.py clean
|
||||
rm PIL/*.so || true
|
||||
find . -name __pycache__ | xargs rm -r
|
||||
rm -r build || true
|
||||
find . -name __pycache__ | xargs rm -r || true
|
||||
|
||||
install:
|
||||
python setup.py install
|
||||
python selftest.py --installed
|
||||
|
||||
test: install
|
||||
test:
|
||||
python test-installed.py
|
||||
|
||||
inplace: clean
|
||||
|
@ -40,3 +53,9 @@ coverage:
|
|||
|
||||
test-dep:
|
||||
pip install coveralls nose nose-cov pep8 pyflakes
|
||||
|
||||
docs:
|
||||
$(MAKE) -C docs html
|
||||
|
||||
docserver:
|
||||
cd docs/_build/html && python -mSimpleHTTPServer 2> /dev/null&
|
|
@ -33,6 +33,7 @@ i32 = _binary.i32le
|
|||
def _accept(prefix):
|
||||
return prefix[:4] == b"\0\0\2\0"
|
||||
|
||||
|
||||
##
|
||||
# Image plugin for Windows Cursor files.
|
||||
|
||||
|
@ -48,7 +49,7 @@ class CurImageFile(BmpImagePlugin.BmpImageFile):
|
|||
# check magic
|
||||
s = self.fp.read(6)
|
||||
if not _accept(s):
|
||||
raise SyntaxError("not an CUR file")
|
||||
raise SyntaxError("not a CUR file")
|
||||
|
||||
# pick the largest cursor in the file
|
||||
m = b""
|
||||
|
@ -58,14 +59,14 @@ class CurImageFile(BmpImagePlugin.BmpImageFile):
|
|||
m = s
|
||||
elif i8(s[0]) > i8(m[0]) and i8(s[1]) > i8(m[1]):
|
||||
m = s
|
||||
#print "width", i8(s[0])
|
||||
#print "height", i8(s[1])
|
||||
#print "colors", i8(s[2])
|
||||
#print "reserved", i8(s[3])
|
||||
#print "hotspot x", i16(s[4:])
|
||||
#print "hotspot y", i16(s[6:])
|
||||
#print "bytes", i32(s[8:])
|
||||
#print "offset", i32(s[12:])
|
||||
# print "width", i8(s[0])
|
||||
# print "height", i8(s[1])
|
||||
# print "colors", i8(s[2])
|
||||
# print "reserved", i8(s[3])
|
||||
# print "hotspot x", i16(s[4:])
|
||||
# print "hotspot y", i16(s[6:])
|
||||
# print "bytes", i32(s[8:])
|
||||
# print "offset", i32(s[12:])
|
||||
|
||||
# load as bitmap
|
||||
self._bitmap(i32(m[12:]) + offset)
|
||||
|
@ -73,7 +74,7 @@ class CurImageFile(BmpImagePlugin.BmpImageFile):
|
|||
# patch up the bitmap height
|
||||
self.size = self.size[0], self.size[1]//2
|
||||
d, e, o, a = self.tile[0]
|
||||
self.tile[0] = d, (0,0)+self.size, o, a
|
||||
self.tile[0] = d, (0, 0)+self.size, o, a
|
||||
|
||||
return
|
||||
|
||||
|
|
|
@ -27,13 +27,15 @@ from PIL import Image, _binary
|
|||
|
||||
from PIL.PcxImagePlugin import PcxImageFile
|
||||
|
||||
MAGIC = 0x3ADE68B1 # QUIZ: what's this value, then?
|
||||
MAGIC = 0x3ADE68B1 # QUIZ: what's this value, then?
|
||||
|
||||
i32 = _binary.i32le
|
||||
|
||||
|
||||
def _accept(prefix):
|
||||
return i32(prefix) == MAGIC
|
||||
|
||||
|
||||
##
|
||||
# Image plugin for the Intel DCX format.
|
||||
|
||||
|
|
|
@ -86,26 +86,32 @@ def Ghostscript(tile, size, fp, scale=1):
|
|||
|
||||
out_fd, outfile = tempfile.mkstemp()
|
||||
os.close(out_fd)
|
||||
in_fd, infile = tempfile.mkstemp()
|
||||
os.close(in_fd)
|
||||
|
||||
# ignore length and offset!
|
||||
# ghostscript can read it
|
||||
# copy whole file to read in ghostscript
|
||||
with open(infile, 'wb') as f:
|
||||
# fetch length of fp
|
||||
fp.seek(0, 2)
|
||||
fsize = fp.tell()
|
||||
# ensure start position
|
||||
# go back
|
||||
fp.seek(0)
|
||||
lengthfile = fsize
|
||||
while lengthfile > 0:
|
||||
s = fp.read(min(lengthfile, 100*1024))
|
||||
if not s:
|
||||
break
|
||||
length -= len(s)
|
||||
f.write(s)
|
||||
|
||||
infile_temp = None
|
||||
if hasattr(fp, 'name') and os.path.exists(fp.name):
|
||||
infile = fp.name
|
||||
else:
|
||||
in_fd, infile_temp = tempfile.mkstemp()
|
||||
os.close(in_fd)
|
||||
infile = infile_temp
|
||||
|
||||
# ignore length and offset!
|
||||
# ghostscript can read it
|
||||
# copy whole file to read in ghostscript
|
||||
with open(infile_temp, 'wb') as f:
|
||||
# fetch length of fp
|
||||
fp.seek(0, 2)
|
||||
fsize = fp.tell()
|
||||
# ensure start position
|
||||
# go back
|
||||
fp.seek(0)
|
||||
lengthfile = fsize
|
||||
while lengthfile > 0:
|
||||
s = fp.read(min(lengthfile, 100*1024))
|
||||
if not s:
|
||||
break
|
||||
lengthfile -= len(s)
|
||||
f.write(s)
|
||||
|
||||
# Build ghostscript command
|
||||
command = ["gs",
|
||||
|
@ -136,49 +142,36 @@ def Ghostscript(tile, size, fp, scale=1):
|
|||
finally:
|
||||
try:
|
||||
os.unlink(outfile)
|
||||
os.unlink(infile)
|
||||
if infile_temo:
|
||||
os.unlink(infile_temp)
|
||||
except: pass
|
||||
|
||||
return im
|
||||
|
||||
|
||||
class PSFile:
|
||||
"""Wrapper that treats either CR or LF as end of line."""
|
||||
"""Wrapper for bytesio object that treats either CR or LF as end of line."""
|
||||
def __init__(self, fp):
|
||||
self.fp = fp
|
||||
self.char = None
|
||||
def __getattr__(self, id):
|
||||
v = getattr(self.fp, id)
|
||||
setattr(self, id, v)
|
||||
return v
|
||||
def seek(self, offset, whence=0):
|
||||
self.char = None
|
||||
self.fp.seek(offset, whence)
|
||||
def read(self, count):
|
||||
return self.fp.read(count).decode('latin-1')
|
||||
def readbinary(self, count):
|
||||
return self.fp.read(count)
|
||||
def tell(self):
|
||||
pos = self.fp.tell()
|
||||
if self.char:
|
||||
pos -= 1
|
||||
return pos
|
||||
def readline(self):
|
||||
s = b""
|
||||
if self.char:
|
||||
c = self.char
|
||||
self.char = None
|
||||
else:
|
||||
c = self.fp.read(1)
|
||||
s = self.char or b""
|
||||
self.char = None
|
||||
|
||||
c = self.fp.read(1)
|
||||
while c not in b"\r\n":
|
||||
s = s + c
|
||||
c = self.fp.read(1)
|
||||
if c == b"\r":
|
||||
self.char = self.fp.read(1)
|
||||
if self.char == b"\n":
|
||||
self.char = None
|
||||
return s.decode('latin-1') + "\n"
|
||||
|
||||
self.char = self.fp.read(1)
|
||||
# line endings can be 1 or 2 of \r \n, in either order
|
||||
if self.char in b"\r\n":
|
||||
self.char = None
|
||||
|
||||
return s.decode('latin-1')
|
||||
|
||||
def _accept(prefix):
|
||||
return prefix[:4] == b"%!PS" or i32(prefix) == 0xC6D3D0C5
|
||||
|
@ -193,36 +186,27 @@ class EpsImageFile(ImageFile.ImageFile):
|
|||
format = "EPS"
|
||||
format_description = "Encapsulated Postscript"
|
||||
|
||||
mode_map = { 1:"L", 2:"LAB", 3:"RGB" }
|
||||
|
||||
def _open(self):
|
||||
(length, offset) = self._find_offset(self.fp)
|
||||
|
||||
fp = PSFile(self.fp)
|
||||
# Rewrap the open file pointer in something that will
|
||||
# convert line endings and decode to latin-1.
|
||||
try:
|
||||
if bytes is str:
|
||||
# Python2, no encoding conversion necessary
|
||||
fp = open(self.fp.name, "Ur")
|
||||
else:
|
||||
# Python3, can use bare open command.
|
||||
fp = open(self.fp.name, "Ur", encoding='latin-1')
|
||||
except Exception as msg:
|
||||
# Expect this for bytesio/stringio
|
||||
fp = PSFile(self.fp)
|
||||
|
||||
# FIX for: Some EPS file not handled correctly / issue #302
|
||||
# EPS can contain binary data
|
||||
# or start directly with latin coding
|
||||
# read header in both ways to handle both
|
||||
# file types
|
||||
# more info see http://partners.adobe.com/public/developer/en/ps/5002.EPSF_Spec.pdf
|
||||
|
||||
# for HEAD without binary preview
|
||||
s = fp.read(4)
|
||||
# for HEAD with binary preview
|
||||
fp.seek(0)
|
||||
sb = fp.readbinary(160)
|
||||
|
||||
if s[:4] == "%!PS":
|
||||
fp.seek(0, 2)
|
||||
length = fp.tell()
|
||||
offset = 0
|
||||
elif i32(sb[0:4]) == 0xC6D3D0C5:
|
||||
offset = i32(sb[4:8])
|
||||
length = i32(sb[8:12])
|
||||
else:
|
||||
raise SyntaxError("not an EPS file")
|
||||
|
||||
# go to offset - start of "%!PS"
|
||||
# go to offset - start of "%!PS"
|
||||
fp.seek(offset)
|
||||
|
||||
|
||||
box = None
|
||||
|
||||
self.mode = "RGB"
|
||||
|
@ -231,18 +215,12 @@ class EpsImageFile(ImageFile.ImageFile):
|
|||
#
|
||||
# Load EPS header
|
||||
|
||||
s = fp.readline()
|
||||
s = fp.readline().strip('\r\n')
|
||||
|
||||
while s:
|
||||
|
||||
if len(s) > 255:
|
||||
raise SyntaxError("not an EPS file")
|
||||
|
||||
if s[-2:] == '\r\n':
|
||||
s = s[:-2]
|
||||
elif s[-1:] == '\n':
|
||||
s = s[:-1]
|
||||
|
||||
try:
|
||||
m = split.match(s)
|
||||
except re.error as v:
|
||||
|
@ -264,9 +242,7 @@ class EpsImageFile(ImageFile.ImageFile):
|
|||
pass
|
||||
|
||||
else:
|
||||
|
||||
m = field.match(s)
|
||||
|
||||
if m:
|
||||
k = m.group(1)
|
||||
|
||||
|
@ -276,16 +252,16 @@ class EpsImageFile(ImageFile.ImageFile):
|
|||
self.info[k[:8]] = k[9:]
|
||||
else:
|
||||
self.info[k] = ""
|
||||
elif s[0:1] == '%':
|
||||
elif s[0] == '%':
|
||||
# handle non-DSC Postscript comments that some
|
||||
# tools mistakenly put in the Comments section
|
||||
pass
|
||||
else:
|
||||
raise IOError("bad EPS header")
|
||||
|
||||
s = fp.readline()
|
||||
s = fp.readline().strip('\r\n')
|
||||
|
||||
if s[:1] != "%":
|
||||
if s[0] != "%":
|
||||
break
|
||||
|
||||
|
||||
|
@ -297,63 +273,48 @@ class EpsImageFile(ImageFile.ImageFile):
|
|||
if len(s) > 255:
|
||||
raise SyntaxError("not an EPS file")
|
||||
|
||||
if s[-2:] == '\r\n':
|
||||
s = s[:-2]
|
||||
elif s[-1:] == '\n':
|
||||
s = s[:-1]
|
||||
|
||||
if s[:11] == "%ImageData:":
|
||||
# Encoded bitmapped image.
|
||||
[x, y, bi, mo, z3, z4, en, id] = s[11:].split(None, 7)
|
||||
|
||||
[x, y, bi, mo, z3, z4, en, id] =\
|
||||
s[11:].split(None, 7)
|
||||
|
||||
x = int(x); y = int(y)
|
||||
|
||||
bi = int(bi)
|
||||
mo = int(mo)
|
||||
|
||||
en = int(en)
|
||||
|
||||
if en == 1:
|
||||
decoder = "eps_binary"
|
||||
elif en == 2:
|
||||
decoder = "eps_hex"
|
||||
else:
|
||||
if int(bi) != 8:
|
||||
break
|
||||
if bi != 8:
|
||||
try:
|
||||
self.mode = self.mode_map[int(mo)]
|
||||
except:
|
||||
break
|
||||
if mo == 1:
|
||||
self.mode = "L"
|
||||
elif mo == 2:
|
||||
self.mode = "LAB"
|
||||
elif mo == 3:
|
||||
self.mode = "RGB"
|
||||
else:
|
||||
break
|
||||
|
||||
if id[:1] == id[-1:] == '"':
|
||||
id = id[1:-1]
|
||||
|
||||
# Scan forward to the actual image data
|
||||
while True:
|
||||
s = fp.readline()
|
||||
if not s:
|
||||
break
|
||||
if s[:len(id)] == id:
|
||||
self.size = x, y
|
||||
self.tile2 = [(decoder,
|
||||
(0, 0, x, y),
|
||||
fp.tell(),
|
||||
0)]
|
||||
return
|
||||
|
||||
s = fp.readline()
|
||||
|
||||
self.size = int(x), int(y)
|
||||
return
|
||||
|
||||
s = fp.readline().strip('\r\n')
|
||||
if not s:
|
||||
break
|
||||
|
||||
if not box:
|
||||
raise IOError("cannot determine EPS bounding box")
|
||||
|
||||
def _find_offset(self, fp):
|
||||
|
||||
s = fp.read(160)
|
||||
|
||||
if s[:4] == b"%!PS":
|
||||
# for HEAD without binary preview
|
||||
fp.seek(0, 2)
|
||||
length = fp.tell()
|
||||
offset = 0
|
||||
elif i32(s[0:4]) == 0xC6D3D0C5:
|
||||
# FIX for: Some EPS file not handled correctly / issue #302
|
||||
# EPS can contain binary data
|
||||
# or start directly with latin coding
|
||||
# more info see http://partners.adobe.com/public/developer/en/ps/5002.EPSF_Spec.pdf
|
||||
offset = i32(s[4:8])
|
||||
length = i32(s[8:12])
|
||||
else:
|
||||
raise SyntaxError("not an EPS file")
|
||||
|
||||
return (length, offset)
|
||||
|
||||
def load(self, scale=1):
|
||||
# Load EPS via Ghostscript
|
||||
if not self.tile:
|
||||
|
|
|
@ -67,8 +67,8 @@ TAGS = {
|
|||
0x0213: "YCbCrPositioning",
|
||||
0x0214: "ReferenceBlackWhite",
|
||||
0x1000: "RelatedImageFileFormat",
|
||||
0x1001: "RelatedImageLength", # FIXME / Dictionary contains duplicate keys
|
||||
0x1001: "RelatedImageWidth", # FIXME \ Dictionary contains duplicate keys
|
||||
0x1001: "RelatedImageWidth",
|
||||
0x1002: "RelatedImageLength",
|
||||
0x828d: "CFARepeatPatternDim",
|
||||
0x828e: "CFAPattern",
|
||||
0x828f: "BatteryLevel",
|
||||
|
|
|
@ -17,8 +17,6 @@
|
|||
import os
|
||||
from PIL import Image, _binary
|
||||
|
||||
import marshal
|
||||
|
||||
try:
|
||||
import zlib
|
||||
except ImportError:
|
||||
|
@ -26,6 +24,7 @@ except ImportError:
|
|||
|
||||
WIDTH = 800
|
||||
|
||||
|
||||
def puti16(fp, values):
|
||||
# write network order (big-endian) 16-bit sequence
|
||||
for v in values:
|
||||
|
@ -33,6 +32,7 @@ def puti16(fp, values):
|
|||
v += 65536
|
||||
fp.write(_binary.o16be(v))
|
||||
|
||||
|
||||
##
|
||||
# Base class for raster font file handlers.
|
||||
|
||||
|
@ -95,9 +95,8 @@ class FontFile:
|
|||
# print chr(i), dst, s
|
||||
self.metrics[i] = d, dst, s
|
||||
|
||||
|
||||
def save1(self, filename):
|
||||
"Save font in version 1 format"
|
||||
def save(self, filename):
|
||||
"Save font"
|
||||
|
||||
self.compile()
|
||||
|
||||
|
@ -107,7 +106,7 @@ class FontFile:
|
|||
# font metrics
|
||||
fp = open(os.path.splitext(filename)[0] + ".pil", "wb")
|
||||
fp.write(b"PILfont\n")
|
||||
fp.write((";;;;;;%d;\n" % self.ysize).encode('ascii')) # HACK!!!
|
||||
fp.write((";;;;;;%d;\n" % self.ysize).encode('ascii')) # HACK!!!
|
||||
fp.write(b"DATA\n")
|
||||
for id in range(256):
|
||||
m = self.metrics[id]
|
||||
|
@ -117,30 +116,4 @@ class FontFile:
|
|||
puti16(fp, m[0] + m[1] + m[2])
|
||||
fp.close()
|
||||
|
||||
|
||||
def save2(self, filename):
|
||||
"Save font in version 2 format"
|
||||
|
||||
# THIS IS WORK IN PROGRESS
|
||||
|
||||
self.compile()
|
||||
|
||||
data = marshal.dumps((self.metrics, self.info))
|
||||
|
||||
if zlib:
|
||||
data = b"z" + zlib.compress(data, 9)
|
||||
else:
|
||||
data = b"u" + data
|
||||
|
||||
fp = open(os.path.splitext(filename)[0] + ".pil", "wb")
|
||||
|
||||
fp.write(b"PILfont2\n" + self.name + "\n" + "DATA\n")
|
||||
|
||||
fp.write(data)
|
||||
|
||||
self.bitmap.save(fp, "PNG")
|
||||
|
||||
fp.close()
|
||||
|
||||
|
||||
save = save1 # for now
|
||||
# End of file
|
||||
|
|
|
@ -96,8 +96,15 @@ class GifImageFile(ImageFile.ImageFile):
|
|||
# rewind
|
||||
self.__offset = 0
|
||||
self.dispose = None
|
||||
self.dispose_extent = [0, 0, 0, 0] #x0, y0, x1, y1
|
||||
self.__frame = -1
|
||||
self.__fp.seek(self.__rewind)
|
||||
self._prev_im = None
|
||||
self.disposal_method = 0
|
||||
else:
|
||||
# ensure that the previous frame was loaded
|
||||
if not self.im:
|
||||
self.load()
|
||||
|
||||
if frame != self.__frame + 1:
|
||||
raise ValueError("cannot seek to frame %d" % frame)
|
||||
|
@ -114,8 +121,7 @@ class GifImageFile(ImageFile.ImageFile):
|
|||
self.__offset = 0
|
||||
|
||||
if self.dispose:
|
||||
self.im = self.dispose
|
||||
self.dispose = None
|
||||
self.im.paste(self.dispose, self.dispose_extent)
|
||||
|
||||
from copy import copy
|
||||
self.palette = copy(self.global_palette)
|
||||
|
@ -140,17 +146,16 @@ class GifImageFile(ImageFile.ImageFile):
|
|||
if flags & 1:
|
||||
self.info["transparency"] = i8(block[3])
|
||||
self.info["duration"] = i16(block[1:3]) * 10
|
||||
try:
|
||||
# disposal methods
|
||||
if flags & 8:
|
||||
# replace with background colour
|
||||
self.dispose = Image.core.fill("P", self.size,
|
||||
self.info["background"])
|
||||
elif flags & 16:
|
||||
# replace with previous contents
|
||||
self.dispose = self.im.copy()
|
||||
except (AttributeError, KeyError):
|
||||
pass
|
||||
|
||||
# disposal method - find the value of bits 4 - 6
|
||||
dispose_bits = 0b00011100 & flags
|
||||
dispose_bits = dispose_bits >> 2
|
||||
if dispose_bits:
|
||||
# only set the dispose if it is not
|
||||
# unspecified. I'm not sure if this is
|
||||
# correct, but it seems to prevent the last
|
||||
# frame from looking odd for some animations
|
||||
self.disposal_method = dispose_bits
|
||||
elif i8(s) == 255:
|
||||
#
|
||||
# application extension
|
||||
|
@ -172,6 +177,7 @@ class GifImageFile(ImageFile.ImageFile):
|
|||
# extent
|
||||
x0, y0 = i16(s[0:]), i16(s[2:])
|
||||
x1, y1 = x0 + i16(s[4:]), y0 + i16(s[6:])
|
||||
self.dispose_extent = x0, y0, x1, y1
|
||||
flags = i8(s[8])
|
||||
|
||||
interlace = (flags & 64) != 0
|
||||
|
@ -194,6 +200,26 @@ class GifImageFile(ImageFile.ImageFile):
|
|||
pass
|
||||
# raise IOError, "illegal GIF tag `%x`" % i8(s)
|
||||
|
||||
try:
|
||||
if self.disposal_method < 2:
|
||||
# do not dispose or none specified
|
||||
self.dispose = None
|
||||
elif self.disposal_method == 2:
|
||||
# replace with background colour
|
||||
self.dispose = Image.core.fill("P", self.size,
|
||||
self.info["background"])
|
||||
else:
|
||||
# replace with previous contents
|
||||
if self.im:
|
||||
self.dispose = self.im.copy()
|
||||
|
||||
# only dispose the extent in this frame
|
||||
if self.dispose:
|
||||
self.dispose = self.dispose.crop(self.dispose_extent)
|
||||
except (AttributeError, KeyError):
|
||||
pass
|
||||
|
||||
|
||||
if not self.tile:
|
||||
# self.__fp = None
|
||||
raise EOFError("no more images in GIF file")
|
||||
|
@ -205,6 +231,18 @@ class GifImageFile(ImageFile.ImageFile):
|
|||
def tell(self):
|
||||
return self.__frame
|
||||
|
||||
def load_end(self):
|
||||
ImageFile.ImageFile.load_end(self)
|
||||
|
||||
# if the disposal method is 'do not dispose', transparent
|
||||
# pixels should show the content of the previous frame
|
||||
if self._prev_im and self.disposal_method == 1:
|
||||
# we do this by pasting the updated area onto the previous
|
||||
# frame which we then use as the current image content
|
||||
updated = self.im.crop(self.dispose_extent)
|
||||
self._prev_im.paste(updated, self.dispose_extent, updated.convert('RGBA'))
|
||||
self.im = self._prev_im
|
||||
self._prev_im = self.im.copy()
|
||||
|
||||
# --------------------------------------------------------------------
|
||||
# Write GIF files
|
||||
|
@ -230,10 +268,9 @@ def _save(im, fp, filename):
|
|||
except IOError:
|
||||
pass # write uncompressed file
|
||||
|
||||
try:
|
||||
rawmode = RAWMODE[im.mode]
|
||||
if im.mode in RAWMODE:
|
||||
imOut = im
|
||||
except KeyError:
|
||||
else:
|
||||
# convert on the fly (EXPERIMENTAL -- I'm not sure PIL
|
||||
# should automatically convert images on save...)
|
||||
if Image.getmodebase(im.mode) == "RGB":
|
||||
|
@ -241,10 +278,8 @@ def _save(im, fp, filename):
|
|||
if im.palette:
|
||||
palette_size = len(im.palette.getdata()[1]) // 3
|
||||
imOut = im.convert("P", palette=1, colors=palette_size)
|
||||
rawmode = "P"
|
||||
else:
|
||||
imOut = im.convert("L")
|
||||
rawmode = "L"
|
||||
|
||||
# header
|
||||
try:
|
||||
|
@ -252,12 +287,6 @@ def _save(im, fp, filename):
|
|||
except KeyError:
|
||||
palette = None
|
||||
im.encoderinfo["optimize"] = im.encoderinfo.get("optimize", True)
|
||||
if im.encoderinfo["optimize"]:
|
||||
# When the mode is L, and we optimize, we end up with
|
||||
# im.mode == P and rawmode = L, which fails.
|
||||
# If we're optimizing the palette, we're going to be
|
||||
# in a rawmode of P anyway.
|
||||
rawmode = 'P'
|
||||
|
||||
header, usedPaletteColors = getheader(imOut, palette, im.encoderinfo)
|
||||
for s in header:
|
||||
|
@ -314,7 +343,7 @@ def _save(im, fp, filename):
|
|||
o8(8)) # bits
|
||||
|
||||
imOut.encoderconfig = (8, interlace)
|
||||
ImageFile._save(imOut, fp, [("gif", (0,0)+im.size, 0, rawmode)])
|
||||
ImageFile._save(imOut, fp, [("gif", (0,0)+im.size, 0, RAWMODE[imOut.mode])])
|
||||
|
||||
fp.write(b"\0") # end of image data
|
||||
|
||||
|
|
|
@ -24,6 +24,7 @@ from PIL._binary import o8
|
|||
|
||||
EPSILON = 1e-10
|
||||
|
||||
|
||||
def linear(middle, pos):
|
||||
if pos <= middle:
|
||||
if middle < EPSILON:
|
||||
|
@ -38,25 +39,30 @@ def linear(middle, pos):
|
|||
else:
|
||||
return 0.5 + 0.5 * pos / middle
|
||||
|
||||
|
||||
def curved(middle, pos):
|
||||
return pos ** (log(0.5) / log(max(middle, EPSILON)))
|
||||
|
||||
|
||||
def sine(middle, pos):
|
||||
return (sin((-pi / 2.0) + pi * linear(middle, pos)) + 1.0) / 2.0
|
||||
|
||||
|
||||
def sphere_increasing(middle, pos):
|
||||
return sqrt(1.0 - (linear(middle, pos) - 1.0) ** 2)
|
||||
|
||||
|
||||
def sphere_decreasing(middle, pos):
|
||||
return 1.0 - sqrt(1.0 - linear(middle, pos) ** 2)
|
||||
|
||||
SEGMENTS = [ linear, curved, sine, sphere_increasing, sphere_decreasing ]
|
||||
SEGMENTS = [linear, curved, sine, sphere_increasing, sphere_decreasing]
|
||||
|
||||
|
||||
class GradientFile:
|
||||
|
||||
gradient = None
|
||||
|
||||
def getpalette(self, entries = 256):
|
||||
def getpalette(self, entries=256):
|
||||
|
||||
palette = []
|
||||
|
||||
|
@ -89,6 +95,7 @@ class GradientFile:
|
|||
|
||||
return b"".join(palette), "RGBA"
|
||||
|
||||
|
||||
##
|
||||
# File handler for GIMP's gradient format.
|
||||
|
||||
|
@ -99,7 +106,13 @@ class GimpGradientFile(GradientFile):
|
|||
if fp.readline()[:13] != b"GIMP Gradient":
|
||||
raise SyntaxError("not a GIMP gradient file")
|
||||
|
||||
count = int(fp.readline())
|
||||
line = fp.readline()
|
||||
|
||||
# GIMP 1.2 gradient files don't contain a name, but GIMP 1.3 files do
|
||||
if line.startswith(b"Name: "):
|
||||
line = fp.readline().strip()
|
||||
|
||||
count = int(line)
|
||||
|
||||
gradient = []
|
||||
|
||||
|
@ -108,13 +121,13 @@ class GimpGradientFile(GradientFile):
|
|||
s = fp.readline().split()
|
||||
w = [float(x) for x in s[:11]]
|
||||
|
||||
x0, x1 = w[0], w[2]
|
||||
xm = w[1]
|
||||
rgb0 = w[3:7]
|
||||
rgb1 = w[7:11]
|
||||
x0, x1 = w[0], w[2]
|
||||
xm = w[1]
|
||||
rgb0 = w[3:7]
|
||||
rgb1 = w[7:11]
|
||||
|
||||
segment = SEGMENTS[int(s[11])]
|
||||
cspace = int(s[12])
|
||||
cspace = int(s[12])
|
||||
|
||||
if cspace != 0:
|
||||
raise IOError("cannot handle HSV colour space")
|
||||
|
|
|
@ -179,6 +179,8 @@ class IcnsFile:
|
|||
i = HEADERSIZE
|
||||
while i < filesize:
|
||||
sig, blocksize = nextheader(fobj)
|
||||
if blocksize <= 0:
|
||||
raise SyntaxError('invalid block header')
|
||||
i += HEADERSIZE
|
||||
blocksize -= HEADERSIZE
|
||||
dct[sig] = (i, blocksize)
|
||||
|
|
46
PIL/Image.py
|
@ -220,6 +220,7 @@ _MODEINFO = {
|
|||
"CMYK": ("RGB", "L", ("C", "M", "Y", "K")),
|
||||
"YCbCr": ("RGB", "L", ("Y", "Cb", "Cr")),
|
||||
"LAB": ("RGB", "L", ("L", "A", "B")),
|
||||
"HSV": ("RGB", "L", ("H", "S", "V")),
|
||||
|
||||
# Experimental modes include I;16, I;16L, I;16B, RGBa, BGR;15, and
|
||||
# BGR;24. Use these modes only if you know exactly what you're
|
||||
|
@ -554,7 +555,6 @@ class Image:
|
|||
self.readonly = 0
|
||||
|
||||
def _dump(self, file=None, format=None):
|
||||
import os
|
||||
import tempfile
|
||||
suffix = ''
|
||||
if format:
|
||||
|
@ -573,6 +573,8 @@ class Image:
|
|||
return file
|
||||
|
||||
def __eq__(self, other):
|
||||
if self.__class__.__name__ != other.__class__.__name__:
|
||||
return False
|
||||
a = (self.mode == other.mode)
|
||||
b = (self.size == other.size)
|
||||
c = (self.getpalette() == other.getpalette())
|
||||
|
@ -1512,6 +1514,9 @@ class Image:
|
|||
|
||||
self.load()
|
||||
|
||||
if self.size == size:
|
||||
return self._new(self.im)
|
||||
|
||||
if self.mode in ("1", "P"):
|
||||
resample = NEAREST
|
||||
|
||||
|
@ -1908,6 +1913,16 @@ class Image:
|
|||
im = self.im.transpose(method)
|
||||
return self._new(im)
|
||||
|
||||
def effect_spread(self, distance):
|
||||
"""
|
||||
Randomly spread pixels in an image.
|
||||
|
||||
:param distance: Distance to spread pixels.
|
||||
"""
|
||||
self.load()
|
||||
im = self.im.effect_spread(distance)
|
||||
return self._new(im)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------
|
||||
# Lazy operations
|
||||
|
@ -2417,3 +2432,32 @@ def _show(image, **options):
|
|||
def _showxv(image, title=None, **options):
|
||||
from PIL import ImageShow
|
||||
ImageShow.show(image, title, **options)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------
|
||||
# Effects
|
||||
|
||||
def effect_mandelbrot(size, extent, quality):
|
||||
"""
|
||||
Generate a Mandelbrot set covering the given extent.
|
||||
|
||||
:param size: The requested size in pixels, as a 2-tuple:
|
||||
(width, height).
|
||||
:param extent: The extent to cover, as a 4-tuple:
|
||||
(x0, y0, x1, y2).
|
||||
:param quality: Quality.
|
||||
"""
|
||||
return Image()._new(core.effect_mandelbrot(size, extent, quality))
|
||||
|
||||
|
||||
def effect_noise(size, sigma):
|
||||
"""
|
||||
Generate Gaussian noise centered around 128.
|
||||
|
||||
:param size: The requested size in pixels, as a 2-tuple:
|
||||
(width, height).
|
||||
:param sigma: Standard deviation of noise.
|
||||
"""
|
||||
return Image()._new(core.effect_noise(size, sigma))
|
||||
|
||||
# End of file
|
||||
|
|
|
@ -1,19 +1,19 @@
|
|||
"""
|
||||
The Python Imaging Library.
|
||||
$Id$
|
||||
## The Python Imaging Library.
|
||||
## $Id$
|
||||
|
||||
Optional color managment support, based on Kevin Cazabon's PyCMS
|
||||
library.
|
||||
## Optional color managment support, based on Kevin Cazabon's PyCMS
|
||||
## library.
|
||||
|
||||
History:
|
||||
2009-03-08 fl Added to PIL.
|
||||
## History:
|
||||
|
||||
Copyright (C) 2002-2003 Kevin Cazabon
|
||||
Copyright (c) 2009 by Fredrik Lundh
|
||||
## 2009-03-08 fl Added to PIL.
|
||||
|
||||
See the README file for information on usage and redistribution. See
|
||||
below for the original description.
|
||||
"""
|
||||
## Copyright (C) 2002-2003 Kevin Cazabon
|
||||
## Copyright (c) 2009 by Fredrik Lundh
|
||||
## Copyright (c) 2013 by Eric Soroos
|
||||
|
||||
## See the README file for information on usage and redistribution. See
|
||||
## below for the original description.
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
|
@ -150,8 +150,13 @@ for flag in FLAGS.values():
|
|||
class ImageCmsProfile:
|
||||
|
||||
def __init__(self, profile):
|
||||
# accepts a string (filename), a file-like object, or a low-level
|
||||
# profile object
|
||||
"""
|
||||
:param profile: Either a string representing a filename,
|
||||
a file like object containing a profile or a
|
||||
low-level profile object
|
||||
|
||||
"""
|
||||
|
||||
if isStringType(profile):
|
||||
self._set(core.profile_open(profile), profile)
|
||||
elif hasattr(profile, "read"):
|
||||
|
@ -169,12 +174,23 @@ class ImageCmsProfile:
|
|||
self.product_name = None
|
||||
self.product_info = None
|
||||
|
||||
def tobytes(self):
|
||||
"""
|
||||
Returns the profile in a format suitable for embedding in
|
||||
saved images.
|
||||
|
||||
:returns: a bytes object containing the ICC profile.
|
||||
"""
|
||||
|
||||
return core.profile_tobytes(self.profile)
|
||||
|
||||
class ImageCmsTransform(Image.ImagePointHandler):
|
||||
|
||||
"""Transform. This can be used with the procedural API, or with the
|
||||
standard Image.point() method.
|
||||
"""
|
||||
# Transform. This can be used with the procedural API, or with the
|
||||
# standard Image.point() method.
|
||||
#
|
||||
# Will return the output profile in the output.info['icc_profile'].
|
||||
|
||||
|
||||
def __init__(self, input, output, input_mode, output_mode,
|
||||
intent=INTENT_PERCEPTUAL, proof=None,
|
||||
|
@ -197,6 +213,8 @@ class ImageCmsTransform(Image.ImagePointHandler):
|
|||
self.input_mode = self.inputMode = input_mode
|
||||
self.output_mode = self.outputMode = output_mode
|
||||
|
||||
self.output_profile = output
|
||||
|
||||
def point(self, im):
|
||||
return self.apply(im)
|
||||
|
||||
|
@ -205,6 +223,7 @@ class ImageCmsTransform(Image.ImagePointHandler):
|
|||
if imOut is None:
|
||||
imOut = Image.new(self.output_mode, im.size, None)
|
||||
self.transform.apply(im.im.id, imOut.im.id)
|
||||
imOut.info['icc_profile'] = self.output_profile.tobytes()
|
||||
return imOut
|
||||
|
||||
def apply_in_place(self, im):
|
||||
|
@ -212,6 +231,7 @@ class ImageCmsTransform(Image.ImagePointHandler):
|
|||
if im.mode != self.output_mode:
|
||||
raise ValueError("mode mismatch") # wrong output mode
|
||||
self.transform.apply(im.im.id, im.im.id)
|
||||
im.info['icc_profile'] = self.output_profile.tobytes()
|
||||
return im
|
||||
|
||||
|
||||
|
@ -570,7 +590,7 @@ def applyTransform(im, transform, inPlace=0):
|
|||
with the transform applied is returned (and im is not changed). The
|
||||
default is False.
|
||||
:returns: Either None, or a new PIL Image object, depending on the value of
|
||||
inPlace
|
||||
inPlace. The profile will be returned in the image's info['icc_profile'].
|
||||
:exception PyCMSError:
|
||||
"""
|
||||
|
||||
|
@ -637,7 +657,7 @@ def getProfileName(profile):
|
|||
|
||||
(pyCMS) Gets the internal product name for the given profile.
|
||||
|
||||
If profile isn't a valid CmsProfile object or filename to a profile,
|
||||
If profile isn't a valid CmsProfile object or filename to a profile,
|
||||
a PyCMSError is raised If an error occurs while trying to obtain the
|
||||
name tag, a PyCMSError is raised.
|
||||
|
||||
|
@ -876,7 +896,7 @@ def isIntentSupported(profile, intent, direction):
|
|||
input/output/proof profile as you desire.
|
||||
|
||||
Some profiles are created specifically for one "direction", can cannot
|
||||
be used for others. Some profiles can only be used for certain
|
||||
be used for others. Some profiles can only be used for certain
|
||||
rendering intents... so it's best to either verify this before trying
|
||||
to create a transform with them (using this function), or catch the
|
||||
potential PyCMSError that will occur if they don't support the modes
|
||||
|
|
|
@ -133,11 +133,27 @@ class ImageFile(Image.Image):
|
|||
return pixel
|
||||
|
||||
self.map = None
|
||||
|
||||
use_mmap = self.filename and len(self.tile) == 1
|
||||
# As of pypy 2.1.0, memory mapping was failing here.
|
||||
use_mmap = use_mmap and not hasattr(sys, 'pypy_version_info')
|
||||
|
||||
readonly = 0
|
||||
|
||||
if self.filename and len(self.tile) == 1 and not hasattr(sys, 'pypy_version_info'):
|
||||
# As of pypy 2.1.0, memory mapping was failing here.
|
||||
# look for read/seek overrides
|
||||
try:
|
||||
read = self.load_read
|
||||
# don't use mmap if there are custom read/seek functions
|
||||
use_mmap = False
|
||||
except AttributeError:
|
||||
read = self.fp.read
|
||||
|
||||
try:
|
||||
seek = self.load_seek
|
||||
use_mmap = False
|
||||
except AttributeError:
|
||||
seek = self.fp.seek
|
||||
|
||||
if use_mmap:
|
||||
# try memory mapping
|
||||
d, e, o, a = self.tile[0]
|
||||
if d == "raw" and a[0] == self.mode and a[0] in Image._MAPMODES:
|
||||
|
@ -165,19 +181,7 @@ class ImageFile(Image.Image):
|
|||
|
||||
self.load_prepare()
|
||||
|
||||
# look for read/seek overrides
|
||||
try:
|
||||
read = self.load_read
|
||||
except AttributeError:
|
||||
read = self.fp.read
|
||||
|
||||
try:
|
||||
seek = self.load_seek
|
||||
except AttributeError:
|
||||
seek = self.fp.seek
|
||||
|
||||
if not self.map:
|
||||
|
||||
# sort tiles in file order
|
||||
self.tile.sort(key=_tilesort)
|
||||
|
||||
|
@ -223,6 +227,8 @@ class ImageFile(Image.Image):
|
|||
break
|
||||
b = b[n:]
|
||||
t = t + n
|
||||
# Need to cleanup here to prevent leaks in PyPy
|
||||
d.cleanup()
|
||||
|
||||
self.tile = []
|
||||
self.readonly = readonly
|
||||
|
@ -467,6 +473,7 @@ def _save(im, fp, tile, bufsize=0):
|
|||
break
|
||||
if s < 0:
|
||||
raise IOError("encoder error %d when writing image file" % s)
|
||||
e.cleanup()
|
||||
else:
|
||||
# slight speedup: compress to real file object
|
||||
for e, b, o, a in tile:
|
||||
|
@ -477,6 +484,7 @@ def _save(im, fp, tile, bufsize=0):
|
|||
s = e.encode_to_file(fh, bufsize)
|
||||
if s < 0:
|
||||
raise IOError("encoder error %d when writing image file" % s)
|
||||
e.cleanup()
|
||||
try:
|
||||
fp.flush()
|
||||
except: pass
|
||||
|
|
|
@ -29,13 +29,15 @@ from __future__ import print_function
|
|||
|
||||
from PIL import Image
|
||||
from PIL._util import isDirectory, isPath
|
||||
import os, sys
|
||||
import os
|
||||
import sys
|
||||
|
||||
try:
|
||||
import warnings
|
||||
except ImportError:
|
||||
warnings = None
|
||||
|
||||
|
||||
class _imagingft_not_installed:
|
||||
# module placeholder
|
||||
def __getattr__(self, id):
|
||||
|
@ -90,8 +92,8 @@ class ImageFont:
|
|||
# read PILfont header
|
||||
if file.readline() != b"PILfont\n":
|
||||
raise SyntaxError("Not a PILfont file")
|
||||
d = file.readline().split(b";")
|
||||
self.info = [] # FIXME: should be a dictionary
|
||||
file.readline().split(b";")
|
||||
self.info = [] # FIXME: should be a dictionary
|
||||
while True:
|
||||
s = file.readline()
|
||||
if not s or s == b"DATA\n":
|
||||
|
@ -113,6 +115,7 @@ class ImageFont:
|
|||
self.getsize = self.font.getsize
|
||||
self.getmask = self.font.getmask
|
||||
|
||||
|
||||
##
|
||||
# Wrapper for FreeType fonts. Application code should use the
|
||||
# <b>truetype</b> factory function to create font objects.
|
||||
|
@ -124,14 +127,18 @@ class FreeTypeFont:
|
|||
# FIXME: use service provider instead
|
||||
if file:
|
||||
if warnings:
|
||||
warnings.warn('file parameter deprecated, please use font parameter instead.', DeprecationWarning)
|
||||
warnings.warn(
|
||||
'file parameter deprecated, '
|
||||
'please use font parameter instead.',
|
||||
DeprecationWarning)
|
||||
font = file
|
||||
|
||||
if isPath(font):
|
||||
self.font = core.getfont(font, size, index, encoding)
|
||||
else:
|
||||
self.font_bytes = font.read()
|
||||
self.font = core.getfont("", size, index, encoding, self.font_bytes)
|
||||
self.font = core.getfont(
|
||||
"", size, index, encoding, self.font_bytes)
|
||||
|
||||
def getname(self):
|
||||
return self.font.family, self.font.style
|
||||
|
@ -140,7 +147,8 @@ class FreeTypeFont:
|
|||
return self.font.ascent, self.font.descent
|
||||
|
||||
def getsize(self, text):
|
||||
return self.font.getsize(text)[0]
|
||||
size, offset = self.font.getsize(text)
|
||||
return (size[0] + offset[0], size[1] + offset[1])
|
||||
|
||||
def getoffset(self, text):
|
||||
return self.font.getsize(text)[1]
|
||||
|
@ -151,7 +159,7 @@ class FreeTypeFont:
|
|||
def getmask2(self, text, mode="", fill=Image.core.fill):
|
||||
size, offset = self.font.getsize(text)
|
||||
im = fill("L", size, 0)
|
||||
self.font.render(text, im.id, mode=="1")
|
||||
self.font.render(text, im.id, mode == "1")
|
||||
return im, offset
|
||||
|
||||
##
|
||||
|
@ -163,12 +171,13 @@ class FreeTypeFont:
|
|||
# be one of Image.FLIP_LEFT_RIGHT, Image.FLIP_TOP_BOTTOM,
|
||||
# Image.ROTATE_90, Image.ROTATE_180, or Image.ROTATE_270.
|
||||
|
||||
|
||||
class TransposedFont:
|
||||
"Wrapper for writing rotated or mirrored text"
|
||||
|
||||
def __init__(self, font, orientation=None):
|
||||
self.font = font
|
||||
self.orientation = orientation # any 'transpose' argument, or None
|
||||
self.orientation = orientation # any 'transpose' argument, or None
|
||||
|
||||
def getsize(self, text):
|
||||
w, h = self.font.getsize(text)
|
||||
|
@ -221,7 +230,10 @@ def truetype(font=None, size=10, index=0, encoding="", filename=None):
|
|||
|
||||
if filename:
|
||||
if warnings:
|
||||
warnings.warn('filename parameter deprecated, please use font parameter instead.', DeprecationWarning)
|
||||
warnings.warn(
|
||||
'filename parameter deprecated, '
|
||||
'please use font parameter instead.',
|
||||
DeprecationWarning)
|
||||
font = filename
|
||||
|
||||
try:
|
||||
|
@ -272,8 +284,8 @@ def load_default():
|
|||
import base64
|
||||
f = ImageFont()
|
||||
f._load_pilfont_data(
|
||||
# courB08
|
||||
BytesIO(base64.decodestring(b'''
|
||||
# courB08
|
||||
BytesIO(base64.decodestring(b'''
|
||||
UElMZm9udAo7Ozs7OzsxMDsKREFUQQoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
|
@ -392,15 +404,4 @@ w7IkEbzhVQAAAABJRU5ErkJggg==
|
|||
'''))))
|
||||
return f
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# create font data chunk for embedding
|
||||
import base64, os, sys
|
||||
font = "../Tests/images/courB08"
|
||||
print(" f._load_pilfont_data(")
|
||||
print(" # %s" % os.path.basename(font))
|
||||
print(" BytesIO(base64.decodestring(b'''")
|
||||
base64.encode(open(font + ".pil", "rb"), sys.stdout)
|
||||
print("''')), Image.open(BytesIO(base64.decodestring(b'''")
|
||||
base64.encode(open(font + ".pbm", "rb"), sys.stdout)
|
||||
print("'''))))")
|
||||
# End of file
|
||||
|
|
|
@ -26,9 +26,11 @@ except ImportError:
|
|||
|
||||
VERBOSE = 0
|
||||
|
||||
|
||||
def _isconstant(v):
|
||||
return isinstance(v, int) or isinstance(v, float)
|
||||
|
||||
|
||||
class _Operand:
|
||||
# wraps an image operand, providing standard operators
|
||||
|
||||
|
@ -68,20 +70,25 @@ class _Operand:
|
|||
im2 = self.__fixup(im2)
|
||||
if im1.mode != im2.mode:
|
||||
# convert both arguments to floating point
|
||||
if im1.mode != "F": im1 = im1.convert("F")
|
||||
if im2.mode != "F": im2 = im2.convert("F")
|
||||
if im1.mode != "F":
|
||||
im1 = im1.convert("F")
|
||||
if im2.mode != "F":
|
||||
im2 = im2.convert("F")
|
||||
if im1.mode != im2.mode:
|
||||
raise ValueError("mode mismatch")
|
||||
if im1.size != im2.size:
|
||||
# crop both arguments to a common size
|
||||
size = (min(im1.size[0], im2.size[0]),
|
||||
min(im1.size[1], im2.size[1]))
|
||||
if im1.size != size: im1 = im1.crop((0, 0) + size)
|
||||
if im2.size != size: im2 = im2.crop((0, 0) + size)
|
||||
if im1.size != size:
|
||||
im1 = im1.crop((0, 0) + size)
|
||||
if im2.size != size:
|
||||
im2 = im2.crop((0, 0) + size)
|
||||
out = Image.new(mode or im1.mode, size, None)
|
||||
else:
|
||||
out = Image.new(mode or im1.mode, im1.size, None)
|
||||
im1.load(); im2.load()
|
||||
im1.load()
|
||||
im2.load()
|
||||
try:
|
||||
op = getattr(_imagingmath, op+"_"+im1.mode)
|
||||
except AttributeError:
|
||||
|
@ -101,34 +108,47 @@ class _Operand:
|
|||
|
||||
def __abs__(self):
|
||||
return self.apply("abs", self)
|
||||
|
||||
def __pos__(self):
|
||||
return self
|
||||
|
||||
def __neg__(self):
|
||||
return self.apply("neg", self)
|
||||
|
||||
# binary operators
|
||||
def __add__(self, other):
|
||||
return self.apply("add", self, other)
|
||||
|
||||
def __radd__(self, other):
|
||||
return self.apply("add", other, self)
|
||||
|
||||
def __sub__(self, other):
|
||||
return self.apply("sub", self, other)
|
||||
|
||||
def __rsub__(self, other):
|
||||
return self.apply("sub", other, self)
|
||||
|
||||
def __mul__(self, other):
|
||||
return self.apply("mul", self, other)
|
||||
|
||||
def __rmul__(self, other):
|
||||
return self.apply("mul", other, self)
|
||||
|
||||
def __truediv__(self, other):
|
||||
return self.apply("div", self, other)
|
||||
|
||||
def __rtruediv__(self, other):
|
||||
return self.apply("div", other, self)
|
||||
|
||||
def __mod__(self, other):
|
||||
return self.apply("mod", self, other)
|
||||
|
||||
def __rmod__(self, other):
|
||||
return self.apply("mod", other, self)
|
||||
|
||||
def __pow__(self, other):
|
||||
return self.apply("pow", self, other)
|
||||
|
||||
def __rpow__(self, other):
|
||||
return self.apply("pow", other, self)
|
||||
|
||||
|
@ -142,54 +162,77 @@ class _Operand:
|
|||
# bitwise
|
||||
def __invert__(self):
|
||||
return self.apply("invert", self)
|
||||
|
||||
def __and__(self, other):
|
||||
return self.apply("and", self, other)
|
||||
|
||||
def __rand__(self, other):
|
||||
return self.apply("and", other, self)
|
||||
|
||||
def __or__(self, other):
|
||||
return self.apply("or", self, other)
|
||||
|
||||
def __ror__(self, other):
|
||||
return self.apply("or", other, self)
|
||||
|
||||
def __xor__(self, other):
|
||||
return self.apply("xor", self, other)
|
||||
|
||||
def __rxor__(self, other):
|
||||
return self.apply("xor", other, self)
|
||||
|
||||
def __lshift__(self, other):
|
||||
return self.apply("lshift", self, other)
|
||||
|
||||
def __rshift__(self, other):
|
||||
return self.apply("rshift", self, other)
|
||||
|
||||
# logical
|
||||
def __eq__(self, other):
|
||||
return self.apply("eq", self, other)
|
||||
|
||||
def __ne__(self, other):
|
||||
return self.apply("ne", self, other)
|
||||
|
||||
def __lt__(self, other):
|
||||
return self.apply("lt", self, other)
|
||||
|
||||
def __le__(self, other):
|
||||
return self.apply("le", self, other)
|
||||
|
||||
def __gt__(self, other):
|
||||
return self.apply("gt", self, other)
|
||||
|
||||
def __ge__(self, other):
|
||||
return self.apply("ge", self, other)
|
||||
|
||||
|
||||
# conversions
|
||||
def imagemath_int(self):
|
||||
return _Operand(self.im.convert("I"))
|
||||
|
||||
|
||||
def imagemath_float(self):
|
||||
return _Operand(self.im.convert("F"))
|
||||
|
||||
|
||||
# logical
|
||||
def imagemath_equal(self, other):
|
||||
return self.apply("eq", self, other, mode="I")
|
||||
|
||||
|
||||
def imagemath_notequal(self, other):
|
||||
return self.apply("ne", self, other, mode="I")
|
||||
|
||||
|
||||
def imagemath_min(self, other):
|
||||
return self.apply("min", self, other)
|
||||
|
||||
|
||||
def imagemath_max(self, other):
|
||||
return self.apply("max", self, other)
|
||||
|
||||
|
||||
def imagemath_convert(self, mode):
|
||||
return _Operand(self.im.convert(mode))
|
||||
|
||||
|
|
|
@ -15,19 +15,19 @@ LUT_SIZE = 1 << 9
|
|||
class LutBuilder:
|
||||
"""A class for building a MorphLut from a descriptive language
|
||||
|
||||
The input patterns is a list of a strings sequences like these:
|
||||
The input patterns is a list of a strings sequences like these::
|
||||
|
||||
4:(...
|
||||
.1.
|
||||
111)->1
|
||||
4:(...
|
||||
.1.
|
||||
111)->1
|
||||
|
||||
(whitespaces including linebreaks are ignored). The option 4
|
||||
describes a series of symmetry operations (in this case a
|
||||
4-rotation), the pattern is described by:
|
||||
|
||||
. or X - Ignore
|
||||
1 - Pixel is on
|
||||
0 - Pixel is off
|
||||
- . or X - Ignore
|
||||
- 1 - Pixel is on
|
||||
- 0 - Pixel is off
|
||||
|
||||
The result of the operation is described after "->" string.
|
||||
|
||||
|
@ -35,15 +35,16 @@ class LutBuilder:
|
|||
returned if no other match is found.
|
||||
|
||||
Operations:
|
||||
4 - 4 way rotation
|
||||
N - Negate
|
||||
1 - Dummy op for no other operation (an op must always be given)
|
||||
M - Mirroring
|
||||
|
||||
- 4 - 4 way rotation
|
||||
- N - Negate
|
||||
- 1 - Dummy op for no other operation (an op must always be given)
|
||||
- M - Mirroring
|
||||
|
||||
Example:
|
||||
|
||||
lb = LutBuilder(patterns = ["4:(... .1. 111)->1"])
|
||||
lut = lb.build_lut()
|
||||
Example::
|
||||
|
||||
lb = LutBuilder(patterns = ["4:(... .1. 111)->1"])
|
||||
lut = lb.build_lut()
|
||||
|
||||
"""
|
||||
def __init__(self, patterns=None, op_name=None):
|
||||
|
|
|
@ -392,7 +392,7 @@ def solarize(image, threshold=128):
|
|||
"""
|
||||
Invert all pixel values above a threshold.
|
||||
|
||||
:param image: The image to posterize.
|
||||
:param image: The image to solarize.
|
||||
:param threshold: All pixels above this greyscale level are inverted.
|
||||
:return: An image.
|
||||
"""
|
||||
|
|
|
@ -17,19 +17,20 @@
|
|||
#
|
||||
|
||||
import array
|
||||
from PIL import Image, ImageColor
|
||||
import warnings
|
||||
from PIL import ImageColor
|
||||
|
||||
|
||||
class ImagePalette:
|
||||
"Color palette for palette mapped images"
|
||||
|
||||
def __init__(self, mode = "RGB", palette = None, size = 0):
|
||||
def __init__(self, mode="RGB", palette=None, size=0):
|
||||
self.mode = mode
|
||||
self.rawmode = None # if set, palette contains raw data
|
||||
self.rawmode = None # if set, palette contains raw data
|
||||
self.palette = palette or list(range(256))*len(self.mode)
|
||||
self.colors = {}
|
||||
self.dirty = None
|
||||
if ((size == 0 and len(self.mode)*256 != len(self.palette)) or
|
||||
if ((size == 0 and len(self.mode)*256 != len(self.palette)) or
|
||||
(size != 0 and size != len(self.palette))):
|
||||
raise ValueError("wrong palette size")
|
||||
|
||||
|
@ -55,7 +56,7 @@ class ImagePalette:
|
|||
return self.palette
|
||||
arr = array.array("B", self.palette)
|
||||
if hasattr(arr, 'tobytes'):
|
||||
#py3k has a tobytes, tostring is deprecated.
|
||||
# py3k has a tobytes, tostring is deprecated.
|
||||
return arr.tobytes()
|
||||
return arr.tostring()
|
||||
|
||||
|
@ -109,6 +110,7 @@ class ImagePalette:
|
|||
fp.write("\n")
|
||||
fp.close()
|
||||
|
||||
|
||||
# --------------------------------------------------------------------
|
||||
# Internal
|
||||
|
||||
|
@ -119,32 +121,53 @@ def raw(rawmode, data):
|
|||
palette.dirty = 1
|
||||
return palette
|
||||
|
||||
|
||||
# --------------------------------------------------------------------
|
||||
# Factories
|
||||
|
||||
def _make_linear_lut(black, white):
|
||||
warnings.warn(
|
||||
'_make_linear_lut() is deprecated. '
|
||||
'Please call make_linear_lut() instead.',
|
||||
DeprecationWarning,
|
||||
stacklevel=2
|
||||
)
|
||||
return make_linear_lut(black, white)
|
||||
|
||||
|
||||
def _make_gamma_lut(exp):
|
||||
warnings.warn(
|
||||
'_make_gamma_lut() is deprecated. '
|
||||
'Please call make_gamma_lut() instead.',
|
||||
DeprecationWarning,
|
||||
stacklevel=2
|
||||
)
|
||||
return make_gamma_lut(exp)
|
||||
|
||||
|
||||
def make_linear_lut(black, white):
|
||||
lut = []
|
||||
if black == 0:
|
||||
for i in range(256):
|
||||
lut.append(white*i//255)
|
||||
else:
|
||||
raise NotImplementedError # FIXME
|
||||
raise NotImplementedError # FIXME
|
||||
return lut
|
||||
|
||||
def _make_gamma_lut(exp, mode="RGB"):
|
||||
|
||||
def make_gamma_lut(exp):
|
||||
lut = []
|
||||
for i in range(256):
|
||||
lut.append(int(((i / 255.0) ** exp) * 255.0 + 0.5))
|
||||
return lut
|
||||
|
||||
def new(mode, data):
|
||||
return Image.core.new_palette(mode, data)
|
||||
|
||||
def negative(mode="RGB"):
|
||||
palette = list(range(256))
|
||||
palette.reverse()
|
||||
return ImagePalette(mode, palette * len(mode))
|
||||
|
||||
|
||||
def random(mode="RGB"):
|
||||
from random import randint
|
||||
palette = []
|
||||
|
@ -152,16 +175,19 @@ def random(mode="RGB"):
|
|||
palette.append(randint(0, 255))
|
||||
return ImagePalette(mode, palette)
|
||||
|
||||
|
||||
def sepia(white="#fff0c0"):
|
||||
r, g, b = ImageColor.getrgb(white)
|
||||
r = _make_linear_lut(0, r)
|
||||
g = _make_linear_lut(0, g)
|
||||
b = _make_linear_lut(0, b)
|
||||
r = make_linear_lut(0, r)
|
||||
g = make_linear_lut(0, g)
|
||||
b = make_linear_lut(0, b)
|
||||
return ImagePalette("RGB", r + g + b)
|
||||
|
||||
|
||||
def wedge(mode="RGB"):
|
||||
return ImagePalette(mode, list(range(256)) * len(mode))
|
||||
|
||||
|
||||
def load(filename):
|
||||
|
||||
# FIXME: supports GIMP gradients only
|
||||
|
@ -177,8 +203,8 @@ def load(filename):
|
|||
p = GimpPaletteFile.GimpPaletteFile(fp)
|
||||
lut = p.getpalette()
|
||||
except (SyntaxError, ValueError):
|
||||
#import traceback
|
||||
#traceback.print_exc()
|
||||
# import traceback
|
||||
# traceback.print_exc()
|
||||
pass
|
||||
|
||||
if not lut:
|
||||
|
@ -188,8 +214,8 @@ def load(filename):
|
|||
p = GimpGradientFile.GimpGradientFile(fp)
|
||||
lut = p.getpalette()
|
||||
except (SyntaxError, ValueError):
|
||||
#import traceback
|
||||
#traceback.print_exc()
|
||||
# import traceback
|
||||
# traceback.print_exc()
|
||||
pass
|
||||
|
||||
if not lut:
|
||||
|
@ -206,4 +232,4 @@ def load(filename):
|
|||
if not lut:
|
||||
raise IOError("cannot load palette")
|
||||
|
||||
return lut # data, rawmode
|
||||
return lut # data, rawmode
|
||||
|
|
|
@ -21,7 +21,8 @@ __version__ = "0.3"
|
|||
|
||||
|
||||
from PIL import Image, ImageFile, _binary
|
||||
import os, tempfile
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
i8 = _binary.i8
|
||||
i16 = _binary.i16be
|
||||
|
@ -35,17 +36,20 @@ COMPRESSION = {
|
|||
|
||||
PAD = o8(0) * 4
|
||||
|
||||
|
||||
#
|
||||
# Helpers
|
||||
|
||||
def i(c):
|
||||
return i32((PAD + c)[-4:])
|
||||
|
||||
|
||||
def dump(c):
|
||||
for i in c:
|
||||
print("%02x" % i8(i), end=' ')
|
||||
print()
|
||||
|
||||
|
||||
##
|
||||
# Image plugin for IPTC/NAA datastreams. To read IPTC/NAA fields
|
||||
# from TIFF and JPEG files, use the <b>getiptcinfo</b> function.
|
||||
|
@ -84,35 +88,13 @@ class IptcImageFile(ImageFile.ImageFile):
|
|||
|
||||
return tag, size
|
||||
|
||||
def _is_raw(self, offset, size):
|
||||
#
|
||||
# check if the file can be mapped
|
||||
|
||||
# DISABLED: the following only slows things down...
|
||||
return 0
|
||||
|
||||
self.fp.seek(offset)
|
||||
t, sz = self.field()
|
||||
if sz != size[0]:
|
||||
return 0
|
||||
y = 1
|
||||
while True:
|
||||
self.fp.seek(sz, 1)
|
||||
t, s = self.field()
|
||||
if t != (8, 10):
|
||||
break
|
||||
if s != sz:
|
||||
return 0
|
||||
y += 1
|
||||
return y == size[1]
|
||||
|
||||
def _open(self):
|
||||
|
||||
# load descriptive fields
|
||||
while True:
|
||||
offset = self.fp.tell()
|
||||
tag, size = self.field()
|
||||
if not tag or tag == (8,10):
|
||||
if not tag or tag == (8, 10):
|
||||
break
|
||||
if size:
|
||||
tagdata = self.fp.read(size)
|
||||
|
@ -129,10 +111,10 @@ class IptcImageFile(ImageFile.ImageFile):
|
|||
# print tag, self.info[tag]
|
||||
|
||||
# mode
|
||||
layers = i8(self.info[(3,60)][0])
|
||||
component = i8(self.info[(3,60)][1])
|
||||
if (3,65) in self.info:
|
||||
id = i8(self.info[(3,65)][0])-1
|
||||
layers = i8(self.info[(3, 60)][0])
|
||||
component = i8(self.info[(3, 60)][1])
|
||||
if (3, 65) in self.info:
|
||||
id = i8(self.info[(3, 65)][0])-1
|
||||
else:
|
||||
id = 0
|
||||
if layers == 1 and not component:
|
||||
|
@ -143,22 +125,18 @@ class IptcImageFile(ImageFile.ImageFile):
|
|||
self.mode = "CMYK"[id]
|
||||
|
||||
# size
|
||||
self.size = self.getint((3,20)), self.getint((3,30))
|
||||
self.size = self.getint((3, 20)), self.getint((3, 30))
|
||||
|
||||
# compression
|
||||
try:
|
||||
compression = COMPRESSION[self.getint((3,120))]
|
||||
compression = COMPRESSION[self.getint((3, 120))]
|
||||
except KeyError:
|
||||
raise IOError("Unknown IPTC image compression")
|
||||
|
||||
# tile
|
||||
if tag == (8,10):
|
||||
if compression == "raw" and self._is_raw(offset, self.size):
|
||||
self.tile = [(compression, (offset, size + 5, -1),
|
||||
(0, 0, self.size[0], self.size[1]))]
|
||||
else:
|
||||
self.tile = [("iptc", (compression, offset),
|
||||
(0, 0, self.size[0], self.size[1]))]
|
||||
if tag == (8, 10):
|
||||
self.tile = [("iptc", (compression, offset),
|
||||
(0, 0, self.size[0], self.size[1]))]
|
||||
|
||||
def load(self):
|
||||
|
||||
|
@ -200,14 +178,17 @@ class IptcImageFile(ImageFile.ImageFile):
|
|||
im.load()
|
||||
self.im = im.im
|
||||
finally:
|
||||
try: os.unlink(outfile)
|
||||
except: pass
|
||||
try:
|
||||
os.unlink(outfile)
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
Image.register_open("IPTC", IptcImageFile)
|
||||
|
||||
Image.register_extension("IPTC", ".iim")
|
||||
|
||||
|
||||
##
|
||||
# Get IPTC information from TIFF, JPEG, or IPTC file.
|
||||
#
|
||||
|
@ -230,11 +211,11 @@ def getiptcinfo(im):
|
|||
# extract the IPTC/NAA resource
|
||||
try:
|
||||
app = im.app["APP13"]
|
||||
if app[:14] == "Photoshop 3.0\x00":
|
||||
if app[:14] == b"Photoshop 3.0\x00":
|
||||
app = app[14:]
|
||||
# parse the image resource block
|
||||
offset = 0
|
||||
while app[offset:offset+4] == "8BIM":
|
||||
while app[offset:offset+4] == b"8BIM":
|
||||
offset += 4
|
||||
# resource code
|
||||
code = JpegImagePlugin.i16(app, offset)
|
||||
|
@ -267,7 +248,7 @@ def getiptcinfo(im):
|
|||
pass
|
||||
|
||||
if data is None:
|
||||
return None # no properties
|
||||
return None # no properties
|
||||
|
||||
# create an IptcImagePlugin object without initializing it
|
||||
class FakeImage:
|
||||
|
@ -282,6 +263,6 @@ def getiptcinfo(im):
|
|||
try:
|
||||
im._open()
|
||||
except (IndexError, KeyError):
|
||||
pass # expected failure
|
||||
pass # expected failure
|
||||
|
||||
return im.info
|
||||
|
|
|
@ -70,6 +70,9 @@ def _parse_jp2_header(fp):
|
|||
else:
|
||||
hlen = 8
|
||||
|
||||
if lbox < hlen:
|
||||
raise SyntaxError('Invalid JP2 header length')
|
||||
|
||||
if tbox == b'jp2h':
|
||||
header = fp.read(lbox - hlen)
|
||||
break
|
||||
|
|
|
@ -36,7 +36,9 @@ __version__ = "0.6"
|
|||
|
||||
import array
|
||||
import struct
|
||||
from PIL import Image, ImageFile, _binary
|
||||
import io
|
||||
from struct import unpack
|
||||
from PIL import Image, ImageFile, TiffImagePlugin, _binary
|
||||
from PIL.JpegPresets import presets
|
||||
from PIL._util import isStringType
|
||||
|
||||
|
@ -110,6 +112,11 @@ def APP(self, marker):
|
|||
pass
|
||||
else:
|
||||
self.info["adobe_transform"] = adobe_transform
|
||||
elif marker == 0xFFE2 and s[:4] == b"MPF\0":
|
||||
# extract MPO information
|
||||
self.info["mp"] = s[4:]
|
||||
# offset is current location minus buffer size plus constant header size
|
||||
self.info["mpoffset"] = self.fp.tell() - n + 4
|
||||
|
||||
|
||||
def COM(self, marker):
|
||||
|
@ -380,18 +387,22 @@ class JpegImageFile(ImageFile.ImageFile):
|
|||
def _getexif(self):
|
||||
return _getexif(self)
|
||||
|
||||
def _getmp(self):
|
||||
return _getmp(self)
|
||||
|
||||
|
||||
def _fixup(value):
|
||||
# Helper function for _getexif() and _getmp()
|
||||
if len(value) == 1:
|
||||
return value[0]
|
||||
return value
|
||||
|
||||
|
||||
def _getexif(self):
|
||||
# Extract EXIF information. This method is highly experimental,
|
||||
# and is likely to be replaced with something better in a future
|
||||
# version.
|
||||
from PIL import TiffImagePlugin
|
||||
import io
|
||||
|
||||
def fixup(value):
|
||||
if len(value) == 1:
|
||||
return value[0]
|
||||
return value
|
||||
# The EXIF record consists of a TIFF file embedded in a JPEG
|
||||
# application marker (!).
|
||||
try:
|
||||
|
@ -405,7 +416,7 @@ def _getexif(self):
|
|||
info = TiffImagePlugin.ImageFileDirectory(head)
|
||||
info.load(file)
|
||||
for key, value in info.items():
|
||||
exif[key] = fixup(value)
|
||||
exif[key] = _fixup(value)
|
||||
# get exif extension
|
||||
try:
|
||||
file.seek(exif[0x8769])
|
||||
|
@ -415,7 +426,7 @@ def _getexif(self):
|
|||
info = TiffImagePlugin.ImageFileDirectory(head)
|
||||
info.load(file)
|
||||
for key, value in info.items():
|
||||
exif[key] = fixup(value)
|
||||
exif[key] = _fixup(value)
|
||||
# get gpsinfo extension
|
||||
try:
|
||||
file.seek(exif[0x8825])
|
||||
|
@ -426,9 +437,77 @@ def _getexif(self):
|
|||
info.load(file)
|
||||
exif[0x8825] = gps = {}
|
||||
for key, value in info.items():
|
||||
gps[key] = fixup(value)
|
||||
gps[key] = _fixup(value)
|
||||
return exif
|
||||
|
||||
|
||||
def _getmp(self):
|
||||
# Extract MP information. This method was inspired by the "highly
|
||||
# experimental" _getexif version that's been in use for years now,
|
||||
# itself based on the ImageFileDirectory class in the TIFF plug-in.
|
||||
|
||||
# The MP record essentially consists of a TIFF file embedded in a JPEG
|
||||
# application marker.
|
||||
try:
|
||||
data = self.info["mp"]
|
||||
except KeyError:
|
||||
return None
|
||||
file = io.BytesIO(data)
|
||||
head = file.read(8)
|
||||
endianness = '>' if head[:4] == b'\x4d\x4d\x00\x2a' else '<'
|
||||
mp = {}
|
||||
# process dictionary
|
||||
info = TiffImagePlugin.ImageFileDirectory(head)
|
||||
info.load(file)
|
||||
for key, value in info.items():
|
||||
mp[key] = _fixup(value)
|
||||
# it's an error not to have a number of images
|
||||
try:
|
||||
quant = mp[0xB001]
|
||||
except KeyError:
|
||||
raise SyntaxError("malformed MP Index (no number of images)")
|
||||
# get MP entries
|
||||
try:
|
||||
mpentries = []
|
||||
for entrynum in range(0, quant):
|
||||
rawmpentry = mp[0xB002][entrynum * 16:(entrynum + 1) * 16]
|
||||
unpackedentry = unpack('{0}LLLHH'.format(endianness), rawmpentry)
|
||||
labels = ('Attribute', 'Size', 'DataOffset', 'EntryNo1', 'EntryNo2')
|
||||
mpentry = dict(zip(labels, unpackedentry))
|
||||
mpentryattr = {
|
||||
'DependentParentImageFlag': bool(mpentry['Attribute'] & (1<<31)),
|
||||
'DependentChildImageFlag': bool(mpentry['Attribute'] & (1<<30)),
|
||||
'RepresentativeImageFlag': bool(mpentry['Attribute'] & (1<<29)),
|
||||
'Reserved': (mpentry['Attribute'] & (3<<27)) >> 27,
|
||||
'ImageDataFormat': (mpentry['Attribute'] & (7<<24)) >> 24,
|
||||
'MPType': mpentry['Attribute'] & 0x00FFFFFF
|
||||
}
|
||||
if mpentryattr['ImageDataFormat'] == 0:
|
||||
mpentryattr['ImageDataFormat'] = 'JPEG'
|
||||
else:
|
||||
raise SyntaxError("unsupported picture format in MPO")
|
||||
mptypemap = {
|
||||
0x000000: 'Undefined',
|
||||
0x010001: 'Large Thumbnail (VGA Equivalent)',
|
||||
0x010002: 'Large Thumbnail (Full HD Equivalent)',
|
||||
0x020001: 'Multi-Frame Image (Panorama)',
|
||||
0x020002: 'Multi-Frame Image: (Disparity)',
|
||||
0x020003: 'Multi-Frame Image: (Multi-Angle)',
|
||||
0x030000: 'Baseline MP Primary Image'
|
||||
}
|
||||
mpentryattr['MPType'] = mptypemap.get(mpentryattr['MPType'],
|
||||
'Unknown')
|
||||
mpentry['Attribute'] = mpentryattr
|
||||
mpentries.append(mpentry)
|
||||
mp[0xB002] = mpentries
|
||||
except KeyError:
|
||||
raise SyntaxError("malformed MP Index (bad MP Entry)")
|
||||
# Next we should try and parse the individual image unique ID list;
|
||||
# we don't because I've never seen this actually used in a real MPO
|
||||
# file and so can't test it.
|
||||
return mp
|
||||
|
||||
|
||||
# --------------------------------------------------------------------
|
||||
# stuff to save JPEG files
|
||||
|
||||
|
@ -466,6 +545,15 @@ def convert_dict_qtables(qtables):
|
|||
|
||||
|
||||
def get_sampling(im):
|
||||
# There's no subsampling when image have only 1 layer
|
||||
# (grayscale images) or when they are CMYK (4 layers),
|
||||
# so set subsampling to default value.
|
||||
#
|
||||
# NOTE: currently Pillow can't encode JPEG to YCCK format.
|
||||
# If YCCK support is added in the future, subsampling code will have
|
||||
# to be updated (here and in JpegEncode.c) to deal with 4 layers.
|
||||
if not hasattr(im, 'layers') or im.layers in (1, 4):
|
||||
return -1
|
||||
sampling = im.layer[0][1:3] + im.layer[1][1:3] + im.layer[2][1:3]
|
||||
return samplings.get(sampling, -1)
|
||||
|
||||
|
@ -611,10 +699,27 @@ def _save_cjpeg(im, fp, filename):
|
|||
except:
|
||||
pass
|
||||
|
||||
|
||||
##
|
||||
# Factory for making JPEG and MPO instances
|
||||
def jpeg_factory(fp=None, filename=None):
|
||||
im = JpegImageFile(fp, filename)
|
||||
mpheader = im._getmp()
|
||||
try:
|
||||
if mpheader[45057] > 1:
|
||||
# It's actually an MPO
|
||||
from .MpoImagePlugin import MpoImageFile
|
||||
im = MpoImageFile(fp, filename)
|
||||
except (TypeError, IndexError):
|
||||
# It is really a JPEG
|
||||
pass
|
||||
return im
|
||||
|
||||
|
||||
# -------------------------------------------------------------------q-
|
||||
# Registry stuff
|
||||
|
||||
Image.register_open("JPEG", JpegImageFile, _accept)
|
||||
Image.register_open("JPEG", jpeg_factory, _accept)
|
||||
Image.register_save("JPEG", _save)
|
||||
|
||||
Image.register_extension("JPEG", ".jfif")
|
||||
|
|
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
|
||||
# -*- coding: latin-1 -*-
|
||||
"""
|
||||
OleFileIO_PL:
|
||||
Module to read Microsoft OLE2 files (also called Structured Storage or
|
||||
Microsoft Compound Document File Format), such as Microsoft Office
|
||||
documents, Image Composer and FlashPix files, Outlook messages, ...
|
||||
This version is compatible with Python 2.6+ and 3.x
|
||||
## OleFileIO_PL:
|
||||
## Module to read Microsoft OLE2 files (also called Structured Storage or
|
||||
## Microsoft Compound Document File Format), such as Microsoft Office
|
||||
## documents, Image Composer and FlashPix files, Outlook messages, ...
|
||||
## This version is compatible with Python 2.6+ and 3.x
|
||||
|
||||
version 0.30 2014-02-04 Philippe Lagadec - http://www.decalage.info
|
||||
## version 0.30 2014-02-04 Philippe Lagadec - http://www.decalage.info
|
||||
|
||||
Project website: http://www.decalage.info/python/olefileio
|
||||
## Project website: http://www.decalage.info/python/olefileio
|
||||
|
||||
Improved version of the OleFileIO module from PIL library v1.1.6
|
||||
See: http://www.pythonware.com/products/pil/index.htm
|
||||
## Improved version of the OleFileIO module from PIL library v1.1.6
|
||||
## See: http://www.pythonware.com/products/pil/index.htm
|
||||
|
||||
The Python Imaging Library (PIL) is
|
||||
Copyright (c) 1997-2005 by Secret Labs AB
|
||||
Copyright (c) 1995-2005 by Fredrik Lundh
|
||||
OleFileIO_PL changes are Copyright (c) 2005-2014 by Philippe Lagadec
|
||||
## The Python Imaging Library (PIL) is
|
||||
|
||||
See source code and LICENSE.txt for information on usage and redistribution.
|
||||
## Copyright (c) 1997-2005 by Secret Labs AB
|
||||
## Copyright (c) 1995-2005 by Fredrik Lundh
|
||||
|
||||
## OleFileIO_PL changes are Copyright (c) 2005-2014 by Philippe Lagadec
|
||||
|
||||
## See source code and LICENSE.txt for information on usage and redistribution.
|
||||
|
||||
## WARNING: THIS IS (STILL) WORK IN PROGRESS.
|
||||
|
||||
WARNING: THIS IS (STILL) WORK IN PROGRESS.
|
||||
"""
|
||||
|
||||
# Starting with OleFileIO_PL v0.30, only Python 2.6+ and 3.x is supported
|
||||
# This import enables print() as a function rather than a keyword
|
||||
|
@ -370,8 +371,9 @@ for key in list(vars().keys()):
|
|||
def isOleFile (filename):
|
||||
"""
|
||||
Test if file is an OLE container (according to its header).
|
||||
filename: file name or path (str, unicode)
|
||||
return: True if OLE, False otherwise.
|
||||
|
||||
:param filename: file name or path (str, unicode)
|
||||
:returns: True if OLE, False otherwise.
|
||||
"""
|
||||
f = open(filename, 'rb')
|
||||
header = f.read(len(MAGIC))
|
||||
|
@ -397,8 +399,8 @@ def i16(c, o = 0):
|
|||
"""
|
||||
Converts a 2-bytes (16 bits) string to an integer.
|
||||
|
||||
c: string containing bytes to convert
|
||||
o: offset of bytes to convert in string
|
||||
:param c: string containing bytes to convert
|
||||
:param o: offset of bytes to convert in string
|
||||
"""
|
||||
return i8(c[o]) | (i8(c[o+1])<<8)
|
||||
|
||||
|
@ -407,8 +409,8 @@ def i32(c, o = 0):
|
|||
"""
|
||||
Converts a 4-bytes (32 bits) string to an integer.
|
||||
|
||||
c: string containing bytes to convert
|
||||
o: offset of bytes to convert in string
|
||||
:param c: string containing bytes to convert
|
||||
:param o: offset of bytes to convert in string
|
||||
"""
|
||||
## return int(ord(c[o])+(ord(c[o+1])<<8)+(ord(c[o+2])<<16)+(ord(c[o+3])<<24))
|
||||
## # [PL]: added int() because "<<" gives long int since Python 2.4
|
||||
|
@ -419,7 +421,8 @@ def i32(c, o = 0):
|
|||
def _clsid(clsid):
|
||||
"""
|
||||
Converts a CLSID to a human-readable string.
|
||||
clsid: string of length 16.
|
||||
|
||||
:param clsid: string of length 16.
|
||||
"""
|
||||
assert len(clsid) == 16
|
||||
# if clsid is only made of null bytes, return an empty string:
|
||||
|
@ -439,8 +442,8 @@ def _unicode(s, errors='replace'):
|
|||
"""
|
||||
Map unicode string to Latin 1. (Python with Unicode support)
|
||||
|
||||
s: UTF-16LE unicode string to convert to Latin-1
|
||||
errors: 'replace', 'ignore' or 'strict'.
|
||||
:param s: UTF-16LE unicode string to convert to Latin-1
|
||||
:param errors: 'replace', 'ignore' or 'strict'.
|
||||
"""
|
||||
#TODO: test if it OleFileIO works with Unicode strings, instead of
|
||||
# converting to Latin-1.
|
||||
|
@ -650,14 +653,14 @@ class _OleStream(io.BytesIO):
|
|||
"""
|
||||
Constructor for _OleStream class.
|
||||
|
||||
fp : file object, the OLE container or the MiniFAT stream
|
||||
sect : sector index of first sector in the stream
|
||||
size : total size of the stream
|
||||
offset : offset in bytes for the first FAT or MiniFAT sector
|
||||
sectorsize: size of one sector
|
||||
fat : array/list of sector indexes (FAT or MiniFAT)
|
||||
filesize : size of OLE file (for debugging)
|
||||
return : a BytesIO instance containing the OLE stream
|
||||
:param fp : file object, the OLE container or the MiniFAT stream
|
||||
:param sect : sector index of first sector in the stream
|
||||
:param size : total size of the stream
|
||||
:param offset : offset in bytes for the first FAT or MiniFAT sector
|
||||
:param sectorsize: size of one sector
|
||||
:param fat : array/list of sector indexes (FAT or MiniFAT)
|
||||
:param filesize : size of OLE file (for debugging)
|
||||
:returns : a BytesIO instance containing the OLE stream
|
||||
"""
|
||||
debug('_OleStream.__init__:')
|
||||
debug(' sect=%d (%X), size=%d, offset=%d, sectorsize=%d, len(fat)=%d, fp=%s'
|
||||
|
@ -793,9 +796,9 @@ class _OleDirectoryEntry:
|
|||
Constructor for an _OleDirectoryEntry object.
|
||||
Parses a 128-bytes entry from the OLE Directory stream.
|
||||
|
||||
entry : string (must be 128 bytes long)
|
||||
sid : index of this directory entry in the OLE file directory
|
||||
olefile: OleFileIO containing this directory entry
|
||||
:param entry : string (must be 128 bytes long)
|
||||
:param sid : index of this directory entry in the OLE file directory
|
||||
:param olefile: OleFileIO containing this directory entry
|
||||
"""
|
||||
self.sid = sid
|
||||
# ref to olefile is stored for future use
|
||||
|
@ -989,7 +992,7 @@ class _OleDirectoryEntry:
|
|||
"""
|
||||
Return modification time of a directory entry.
|
||||
|
||||
return: None if modification time is null, a python datetime object
|
||||
:returns: None if modification time is null, a python datetime object
|
||||
otherwise (UTC timezone)
|
||||
|
||||
new in version 0.26
|
||||
|
@ -1003,7 +1006,7 @@ class _OleDirectoryEntry:
|
|||
"""
|
||||
Return creation time of a directory entry.
|
||||
|
||||
return: None if modification time is null, a python datetime object
|
||||
:returns: None if modification time is null, a python datetime object
|
||||
otherwise (UTC timezone)
|
||||
|
||||
new in version 0.26
|
||||
|
@ -1020,7 +1023,8 @@ class OleFileIO:
|
|||
OLE container object
|
||||
|
||||
This class encapsulates the interface to an OLE 2 structured
|
||||
storage file. Use the {@link listdir} and {@link openstream} methods to
|
||||
storage file. Use the :py:meth:`~PIL.OleFileIO.OleFileIO.listdir` and
|
||||
:py:meth:`~PIL.OleFileIO.OleFileIO.openstream` methods to
|
||||
access the contents of this file.
|
||||
|
||||
Object names are given as a list of strings, one for each subentry
|
||||
|
@ -1048,8 +1052,8 @@ class OleFileIO:
|
|||
"""
|
||||
Constructor for OleFileIO class.
|
||||
|
||||
filename: file to open.
|
||||
raise_defects: minimal level for defects to be raised as exceptions.
|
||||
:param filename: file to open.
|
||||
:param raise_defects: minimal level for defects to be raised as exceptions.
|
||||
(use DEFECT_FATAL for a typical application, DEFECT_INCORRECT for a
|
||||
security-oriented application, see source code for details)
|
||||
"""
|
||||
|
@ -1068,13 +1072,13 @@ class OleFileIO:
|
|||
It may raise an IOError exception according to the minimal level chosen
|
||||
for the OleFileIO object.
|
||||
|
||||
defect_level: defect level, possible values are:
|
||||
:param defect_level: defect level, possible values are:
|
||||
DEFECT_UNSURE : a case which looks weird, but not sure it's a defect
|
||||
DEFECT_POTENTIAL : a potential defect
|
||||
DEFECT_INCORRECT : an error according to specifications, but parsing can go on
|
||||
DEFECT_FATAL : an error which cannot be ignored, parsing is impossible
|
||||
message: string describing the defect, used with raised exception.
|
||||
exception_type: exception class to be raised, IOError by default
|
||||
:param message: string describing the defect, used with raised exception.
|
||||
:param exception_type: exception class to be raised, IOError by default
|
||||
"""
|
||||
# added by [PL]
|
||||
if defect_level >= self._raise_defects_level:
|
||||
|
@ -1089,7 +1093,7 @@ class OleFileIO:
|
|||
Open an OLE2 file.
|
||||
Reads the header, FAT and directory.
|
||||
|
||||
filename: string-like or file-like object
|
||||
:param filename: string-like or file-like object
|
||||
"""
|
||||
#[PL] check if filename is a string-like or file-like object:
|
||||
# (it is better to check for a read() method)
|
||||
|
@ -1276,8 +1280,8 @@ class OleFileIO:
|
|||
Checks if a stream has not been already referenced elsewhere.
|
||||
This method should only be called once for each known stream, and only
|
||||
if stream size is not null.
|
||||
first_sect: index of first sector of the stream in FAT
|
||||
minifat: if True, stream is located in the MiniFAT, else in the FAT
|
||||
:param first_sect: index of first sector of the stream in FAT
|
||||
:param minifat: if True, stream is located in the MiniFAT, else in the FAT
|
||||
"""
|
||||
if minifat:
|
||||
debug('_check_duplicate_stream: sect=%d in MiniFAT' % first_sect)
|
||||
|
@ -1371,8 +1375,9 @@ class OleFileIO:
|
|||
def loadfat_sect(self, sect):
|
||||
"""
|
||||
Adds the indexes of the given sector to the FAT
|
||||
sect: string containing the first FAT sector, or array of long integers
|
||||
return: index of last FAT sector.
|
||||
|
||||
:param sect: string containing the first FAT sector, or array of long integers
|
||||
:returns: index of last FAT sector.
|
||||
"""
|
||||
# a FAT sector is an array of ulong integers.
|
||||
if isinstance(sect, array.array):
|
||||
|
@ -1505,8 +1510,9 @@ class OleFileIO:
|
|||
def getsect(self, sect):
|
||||
"""
|
||||
Read given sector from file on disk.
|
||||
sect: sector index
|
||||
returns a string containing the sector data.
|
||||
|
||||
:param sect: sector index
|
||||
:returns: a string containing the sector data.
|
||||
"""
|
||||
# [PL] this original code was wrong when sectors are 4KB instead of
|
||||
# 512 bytes:
|
||||
|
@ -1530,7 +1536,8 @@ class OleFileIO:
|
|||
def loaddirectory(self, sect):
|
||||
"""
|
||||
Load the directory.
|
||||
sect: sector index of directory stream.
|
||||
|
||||
:param sect: sector index of directory stream.
|
||||
"""
|
||||
# The directory is stored in a standard
|
||||
# substream, independent of its size.
|
||||
|
@ -1567,9 +1574,10 @@ class OleFileIO:
|
|||
Load a directory entry from the directory.
|
||||
This method should only be called once for each storage/stream when
|
||||
loading the directory.
|
||||
sid: index of storage/stream in the directory.
|
||||
return: a _OleDirectoryEntry object
|
||||
raise: IOError if the entry has always been referenced.
|
||||
|
||||
:param sid: index of storage/stream in the directory.
|
||||
:returns: a _OleDirectoryEntry object
|
||||
:exception IOError: if the entry has always been referenced.
|
||||
"""
|
||||
# check if SID is OK:
|
||||
if sid<0 or sid>=len(self.direntries):
|
||||
|
@ -1598,9 +1606,9 @@ class OleFileIO:
|
|||
Open a stream, either in FAT or MiniFAT according to its size.
|
||||
(openstream helper)
|
||||
|
||||
start: index of first sector
|
||||
size: size of stream (or nothing if size is unknown)
|
||||
force_FAT: if False (default), stream will be opened in FAT or MiniFAT
|
||||
:param start: index of first sector
|
||||
:param size: size of stream (or nothing if size is unknown)
|
||||
:param force_FAT: if False (default), stream will be opened in FAT or MiniFAT
|
||||
according to size. If True, it will always be opened in FAT.
|
||||
"""
|
||||
debug('OleFileIO.open(): sect=%d, size=%d, force_FAT=%s' %
|
||||
|
@ -1630,11 +1638,11 @@ class OleFileIO:
|
|||
def _list(self, files, prefix, node, streams=True, storages=False):
|
||||
"""
|
||||
(listdir helper)
|
||||
files: list of files to fill in
|
||||
prefix: current location in storage tree (list of names)
|
||||
node: current node (_OleDirectoryEntry object)
|
||||
streams: bool, include streams if True (True by default) - new in v0.26
|
||||
storages: bool, include storages if True (False by default) - new in v0.26
|
||||
:param files: list of files to fill in
|
||||
:param prefix: current location in storage tree (list of names)
|
||||
:param node: current node (_OleDirectoryEntry object)
|
||||
:param streams: bool, include streams if True (True by default) - new in v0.26
|
||||
:param storages: bool, include storages if True (False by default) - new in v0.26
|
||||
(note: the root storage is never included)
|
||||
"""
|
||||
prefix = prefix + [node.name]
|
||||
|
@ -1657,9 +1665,9 @@ class OleFileIO:
|
|||
"""
|
||||
Return a list of streams stored in this file
|
||||
|
||||
streams: bool, include streams if True (True by default) - new in v0.26
|
||||
storages: bool, include storages if True (False by default) - new in v0.26
|
||||
(note: the root storage is never included)
|
||||
:param streams: bool, include streams if True (True by default) - new in v0.26
|
||||
:param storages: bool, include storages if True (False by default) - new in v0.26
|
||||
(note: the root storage is never included)
|
||||
"""
|
||||
files = []
|
||||
self._list(files, [], self.root, streams, storages)
|
||||
|
@ -1671,12 +1679,13 @@ class OleFileIO:
|
|||
Returns directory entry of given filename. (openstream helper)
|
||||
Note: this method is case-insensitive.
|
||||
|
||||
filename: path of stream in storage tree (except root entry), either:
|
||||
:param filename: path of stream in storage tree (except root entry), either:
|
||||
|
||||
- a string using Unix path syntax, for example:
|
||||
'storage_1/storage_1.2/stream'
|
||||
- a list of storage filenames, path to the desired stream/storage.
|
||||
Example: ['storage_1', 'storage_1.2', 'stream']
|
||||
return: sid of requested filename
|
||||
:returns: sid of requested filename
|
||||
raise IOError if file not found
|
||||
"""
|
||||
|
||||
|
@ -1700,13 +1709,15 @@ class OleFileIO:
|
|||
"""
|
||||
Open a stream as a read-only file object (BytesIO).
|
||||
|
||||
filename: path of stream in storage tree (except root entry), either:
|
||||
:param filename: path of stream in storage tree (except root entry), either:
|
||||
|
||||
- a string using Unix path syntax, for example:
|
||||
'storage_1/storage_1.2/stream'
|
||||
- a list of storage filenames, path to the desired stream/storage.
|
||||
Example: ['storage_1', 'storage_1.2', 'stream']
|
||||
return: file object (read-only)
|
||||
raise IOError if filename not found, or if this is not a stream.
|
||||
|
||||
:returns: file object (read-only)
|
||||
:exception IOError: if filename not found, or if this is not a stream.
|
||||
"""
|
||||
sid = self._find(filename)
|
||||
entry = self.direntries[sid]
|
||||
|
@ -1720,8 +1731,9 @@ class OleFileIO:
|
|||
Test if given filename exists as a stream or a storage in the OLE
|
||||
container, and return its type.
|
||||
|
||||
filename: path of stream in storage tree. (see openstream for syntax)
|
||||
return: False if object does not exist, its entry type (>0) otherwise:
|
||||
:param filename: path of stream in storage tree. (see openstream for syntax)
|
||||
:returns: False if object does not exist, its entry type (>0) otherwise:
|
||||
|
||||
- STGTY_STREAM: a stream
|
||||
- STGTY_STORAGE: a storage
|
||||
- STGTY_ROOT: the root entry
|
||||
|
@ -1738,10 +1750,10 @@ class OleFileIO:
|
|||
"""
|
||||
Return modification time of a stream/storage.
|
||||
|
||||
filename: path of stream/storage in storage tree. (see openstream for
|
||||
syntax)
|
||||
return: None if modification time is null, a python datetime object
|
||||
otherwise (UTC timezone)
|
||||
:param filename: path of stream/storage in storage tree. (see openstream for
|
||||
syntax)
|
||||
:returns: None if modification time is null, a python datetime object
|
||||
otherwise (UTC timezone)
|
||||
|
||||
new in version 0.26
|
||||
"""
|
||||
|
@ -1754,10 +1766,10 @@ class OleFileIO:
|
|||
"""
|
||||
Return creation time of a stream/storage.
|
||||
|
||||
filename: path of stream/storage in storage tree. (see openstream for
|
||||
syntax)
|
||||
return: None if creation time is null, a python datetime object
|
||||
otherwise (UTC timezone)
|
||||
:param filename: path of stream/storage in storage tree. (see openstream for
|
||||
syntax)
|
||||
:returns: None if creation time is null, a python datetime object
|
||||
otherwise (UTC timezone)
|
||||
|
||||
new in version 0.26
|
||||
"""
|
||||
|
@ -1771,8 +1783,8 @@ class OleFileIO:
|
|||
Test if given filename exists as a stream or a storage in the OLE
|
||||
container.
|
||||
|
||||
filename: path of stream in storage tree. (see openstream for syntax)
|
||||
return: True if object exist, else False.
|
||||
:param filename: path of stream in storage tree. (see openstream for syntax)
|
||||
:returns: True if object exist, else False.
|
||||
"""
|
||||
try:
|
||||
sid = self._find(filename)
|
||||
|
@ -1785,9 +1797,10 @@ class OleFileIO:
|
|||
"""
|
||||
Return size of a stream in the OLE container, in bytes.
|
||||
|
||||
filename: path of stream in storage tree (see openstream for syntax)
|
||||
return: size in bytes (long integer)
|
||||
raise: IOError if file not found, TypeError if this is not a stream.
|
||||
:param filename: path of stream in storage tree (see openstream for syntax)
|
||||
:returns: size in bytes (long integer)
|
||||
:exception IOError: if file not found
|
||||
:exception TypeError: if this is not a stream
|
||||
"""
|
||||
sid = self._find(filename)
|
||||
entry = self.direntries[sid]
|
||||
|
@ -1809,11 +1822,11 @@ class OleFileIO:
|
|||
"""
|
||||
Return properties described in substream.
|
||||
|
||||
filename: path of stream in storage tree (see openstream for syntax)
|
||||
convert_time: bool, if True timestamps will be converted to Python datetime
|
||||
no_conversion: None or list of int, timestamps not to be converted
|
||||
(for example total editing time is not a real timestamp)
|
||||
return: a dictionary of values indexed by id (integer)
|
||||
:param filename: path of stream in storage tree (see openstream for syntax)
|
||||
:param convert_time: bool, if True timestamps will be converted to Python datetime
|
||||
:param no_conversion: None or list of int, timestamps not to be converted
|
||||
(for example total editing time is not a real timestamp)
|
||||
:returns: a dictionary of values indexed by id (integer)
|
||||
"""
|
||||
# make sure no_conversion is a list, just to simplify code below:
|
||||
if no_conversion == None:
|
||||
|
|
|
@ -73,9 +73,8 @@ class PSDraw:
|
|||
|
||||
def setink(self, ink):
|
||||
"""
|
||||
.. warning::
|
||||
.. warning:: This has been in the PIL API for ages but was never implemented.
|
||||
|
||||
This has been in the PIL API for ages but was never implemented.
|
||||
"""
|
||||
print("*** NOT YET IMPLEMENTED ***")
|
||||
|
||||
|
|
|
@ -147,6 +147,17 @@ class ChunkStream:
|
|||
|
||||
return cids
|
||||
|
||||
# --------------------------------------------------------------------
|
||||
# Subclass of string to allow iTXt chunks to look like strings while
|
||||
# keeping their extra information
|
||||
|
||||
class iTXt(str):
|
||||
@staticmethod
|
||||
def __new__(cls, text, lang, tkey):
|
||||
self = str.__new__(cls, text)
|
||||
self.lang = lang
|
||||
self.tkey = tkey
|
||||
return self
|
||||
|
||||
# --------------------------------------------------------------------
|
||||
# PNG chunk container (for use with save(pnginfo=))
|
||||
|
@ -159,14 +170,36 @@ class PngInfo:
|
|||
def add(self, cid, data):
|
||||
self.chunks.append((cid, data))
|
||||
|
||||
def add_itxt(self, key, value, lang="", tkey="", zip=False):
|
||||
if not isinstance(key, bytes):
|
||||
key = key.encode("latin-1", "strict")
|
||||
if not isinstance(value, bytes):
|
||||
value = value.encode("utf-8", "strict")
|
||||
if not isinstance(lang, bytes):
|
||||
lang = lang.encode("utf-8", "strict")
|
||||
if not isinstance(tkey, bytes):
|
||||
tkey = tkey.encode("utf-8", "strict")
|
||||
|
||||
if zip:
|
||||
import zlib
|
||||
self.add(b"iTXt", key + b"\0\x01\0" + lang + b"\0" + tkey + b"\0" + zlib.compress(value))
|
||||
else:
|
||||
self.add(b"iTXt", key + b"\0\0\0" + lang + b"\0" + tkey + b"\0" + value)
|
||||
|
||||
def add_text(self, key, value, zip=0):
|
||||
if isinstance(value, iTXt):
|
||||
return self.add_itxt(key, value, value.lang, value.tkey, bool(zip))
|
||||
|
||||
# The tEXt chunk stores latin-1 text
|
||||
if not isinstance(value, bytes):
|
||||
try:
|
||||
value = value.encode('latin-1', 'strict')
|
||||
except UnicodeError:
|
||||
return self.add_itxt(key, value, zip=bool(zip))
|
||||
|
||||
if not isinstance(key, bytes):
|
||||
key = key.encode('latin-1', 'strict')
|
||||
|
||||
if not isinstance(value, bytes):
|
||||
value = value.encode('latin-1', 'replace')
|
||||
|
||||
if zip:
|
||||
import zlib
|
||||
self.add(b"zTXt", key + b"\0\0" + zlib.compress(value))
|
||||
|
@ -329,6 +362,43 @@ class PngStream(ChunkStream):
|
|||
self.im_info[k] = self.im_text[k] = v
|
||||
return s
|
||||
|
||||
def chunk_iTXt(self, pos, length):
|
||||
|
||||
# international text
|
||||
r = s = ImageFile._safe_read(self.fp, length)
|
||||
try:
|
||||
k, r = r.split(b"\0", 1)
|
||||
except ValueError:
|
||||
return s
|
||||
if len(r) < 2:
|
||||
return s
|
||||
cf, cm, r = i8(r[0]), i8(r[1]), r[2:]
|
||||
try:
|
||||
lang, tk, v = r.split(b"\0", 2)
|
||||
except ValueError:
|
||||
return s
|
||||
if cf != 0:
|
||||
if cm == 0:
|
||||
import zlib
|
||||
try:
|
||||
v = zlib.decompress(v)
|
||||
except zlib.error:
|
||||
return s
|
||||
else:
|
||||
return s
|
||||
if bytes is not str:
|
||||
try:
|
||||
k = k.decode("latin-1", "strict")
|
||||
lang = lang.decode("utf-8", "strict")
|
||||
tk = tk.decode("utf-8", "strict")
|
||||
v = v.decode("utf-8", "strict")
|
||||
except UnicodeError:
|
||||
return s
|
||||
|
||||
self.im_info[k] = self.im_text[k] = iTXt(v, lang, tk)
|
||||
|
||||
return s
|
||||
|
||||
# --------------------------------------------------------------------
|
||||
# PNG reader
|
||||
|
||||
|
|
|
@ -261,6 +261,7 @@ mode_map = {'1': _PyAccess8,
|
|||
'PA': _PyAccess32_2,
|
||||
'RGB': _PyAccess32_3,
|
||||
'LAB': _PyAccess32_3,
|
||||
'HSV': _PyAccess32_3,
|
||||
'YCbCr': _PyAccess32_3,
|
||||
'RGBA': _PyAccess32_4,
|
||||
'RGBa': _PyAccess32_4,
|
||||
|
|
|
@ -31,6 +31,7 @@ i32 = _binary.i32be
|
|||
def _accept(prefix):
|
||||
return i16(prefix) == 474
|
||||
|
||||
|
||||
##
|
||||
# Image plugin for SGI images.
|
||||
|
||||
|
@ -44,7 +45,7 @@ class SgiImageFile(ImageFile.ImageFile):
|
|||
# HEAD
|
||||
s = self.fp.read(512)
|
||||
if i16(s) != 474:
|
||||
raise SyntaxError("not an SGI image file")
|
||||
raise ValueError("Not an SGI image file")
|
||||
|
||||
# relevant header entries
|
||||
compression = i8(s[2])
|
||||
|
@ -60,22 +61,22 @@ class SgiImageFile(ImageFile.ImageFile):
|
|||
elif layout == (1, 3, 4):
|
||||
self.mode = "RGBA"
|
||||
else:
|
||||
raise SyntaxError("unsupported SGI image mode")
|
||||
raise ValueError("Unsupported SGI image mode")
|
||||
|
||||
# size
|
||||
self.size = i16(s[6:]), i16(s[8:])
|
||||
|
||||
|
||||
# decoder info
|
||||
if compression == 0:
|
||||
offset = 512
|
||||
pagesize = self.size[0]*self.size[1]*layout[0]
|
||||
self.tile = []
|
||||
for layer in self.mode:
|
||||
self.tile.append(("raw", (0,0)+self.size, offset, (layer,0,-1)))
|
||||
self.tile.append(
|
||||
("raw", (0, 0)+self.size, offset, (layer, 0, -1)))
|
||||
offset = offset + pagesize
|
||||
elif compression == 1:
|
||||
self.tile = [("sgi_rle", (0,0)+self.size, 512, (self.mode, 0, -1))]
|
||||
raise ValueError("SGI RLE encoding not supported")
|
||||
|
||||
#
|
||||
# registry
|
||||
|
@ -85,5 +86,6 @@ Image.register_open("SGI", SgiImageFile, _accept)
|
|||
Image.register_extension("SGI", ".bw")
|
||||
Image.register_extension("SGI", ".rgb")
|
||||
Image.register_extension("SGI", ".rgba")
|
||||
Image.register_extension("SGI", ".sgi")
|
||||
|
||||
Image.register_extension("SGI", ".sgi") # really?
|
||||
# End of file
|
||||
|
|
|
@ -29,6 +29,7 @@ i32 = _binary.i32be
|
|||
def _accept(prefix):
|
||||
return i32(prefix) == 0x59a66a95
|
||||
|
||||
|
||||
##
|
||||
# Image plugin for Sun raster files.
|
||||
|
||||
|
@ -70,9 +71,9 @@ class SunImageFile(ImageFile.ImageFile):
|
|||
stride = (((self.size[0] * depth + 7) // 8) + 3) & (~3)
|
||||
|
||||
if compression == 1:
|
||||
self.tile = [("raw", (0,0)+self.size, offset, (rawmode, stride))]
|
||||
self.tile = [("raw", (0, 0)+self.size, offset, (rawmode, stride))]
|
||||
elif compression == 2:
|
||||
self.tile = [("sun_rle", (0,0)+self.size, offset, rawmode)]
|
||||
self.tile = [("sun_rle", (0, 0)+self.size, offset, rawmode)]
|
||||
|
||||
#
|
||||
# registry
|
||||
|
|
|
@ -42,9 +42,6 @@ MODES = {
|
|||
}
|
||||
|
||||
|
||||
def _accept(prefix):
|
||||
return prefix[0:1] == b"\0"
|
||||
|
||||
##
|
||||
# Image plugin for Targa files.
|
||||
|
||||
|
@ -58,7 +55,7 @@ class TgaImageFile(ImageFile.ImageFile):
|
|||
# process header
|
||||
s = self.fp.read(18)
|
||||
|
||||
id = i8(s[0])
|
||||
idlen = i8(s[0])
|
||||
|
||||
colormaptype = i8(s[1])
|
||||
imagetype = i8(s[2])
|
||||
|
@ -70,7 +67,7 @@ class TgaImageFile(ImageFile.ImageFile):
|
|||
self.size = i16(s[12:]), i16(s[14:])
|
||||
|
||||
# validate header fields
|
||||
if id != 0 or colormaptype not in (0, 1) or\
|
||||
if colormaptype not in (0, 1) or\
|
||||
self.size[0] <= 0 or self.size[1] <= 0 or\
|
||||
depth not in (1, 8, 16, 24, 32):
|
||||
raise SyntaxError("not a TGA file")
|
||||
|
@ -79,7 +76,7 @@ class TgaImageFile(ImageFile.ImageFile):
|
|||
if imagetype in (3, 11):
|
||||
self.mode = "L"
|
||||
if depth == 1:
|
||||
self.mode = "1" # ???
|
||||
self.mode = "1" # ???
|
||||
elif imagetype in (1, 9):
|
||||
self.mode = "P"
|
||||
elif imagetype in (2, 10):
|
||||
|
@ -103,22 +100,25 @@ class TgaImageFile(ImageFile.ImageFile):
|
|||
if imagetype & 8:
|
||||
self.info["compression"] = "tga_rle"
|
||||
|
||||
if idlen:
|
||||
self.info["id_section"] = self.fp.read(idlen)
|
||||
|
||||
if colormaptype:
|
||||
# read palette
|
||||
start, size, mapdepth = i16(s[3:]), i16(s[5:]), i16(s[7:])
|
||||
if mapdepth == 16:
|
||||
self.palette = ImagePalette.raw("BGR;16",
|
||||
b"\0"*2*start + self.fp.read(2*size))
|
||||
self.palette = ImagePalette.raw(
|
||||
"BGR;16", b"\0"*2*start + self.fp.read(2*size))
|
||||
elif mapdepth == 24:
|
||||
self.palette = ImagePalette.raw("BGR",
|
||||
b"\0"*3*start + self.fp.read(3*size))
|
||||
self.palette = ImagePalette.raw(
|
||||
"BGR", b"\0"*3*start + self.fp.read(3*size))
|
||||
elif mapdepth == 32:
|
||||
self.palette = ImagePalette.raw("BGRA",
|
||||
b"\0"*4*start + self.fp.read(4*size))
|
||||
self.palette = ImagePalette.raw(
|
||||
"BGRA", b"\0"*4*start + self.fp.read(4*size))
|
||||
|
||||
# setup tile descriptor
|
||||
try:
|
||||
rawmode = MODES[(imagetype&7, depth)]
|
||||
rawmode = MODES[(imagetype & 7, depth)]
|
||||
if imagetype & 8:
|
||||
# compressed
|
||||
self.tile = [("tga_rle", (0, 0)+self.size,
|
||||
|
@ -127,7 +127,7 @@ class TgaImageFile(ImageFile.ImageFile):
|
|||
self.tile = [("raw", (0, 0)+self.size,
|
||||
self.fp.tell(), (rawmode, 0, orientation))]
|
||||
except KeyError:
|
||||
pass # cannot decode
|
||||
pass # cannot decode
|
||||
|
||||
#
|
||||
# --------------------------------------------------------------------
|
||||
|
@ -145,6 +145,7 @@ SAVE = {
|
|||
"RGBA": ("BGRA", 32, 0, 2),
|
||||
}
|
||||
|
||||
|
||||
def _save(im, fp, filename, check=0):
|
||||
|
||||
try:
|
||||
|
@ -185,13 +186,14 @@ def _save(im, fp, filename, check=0):
|
|||
if colormaptype:
|
||||
fp.write(im.im.getpalette("RGB", "BGR"))
|
||||
|
||||
ImageFile._save(im, fp, [("raw", (0,0)+im.size, 0, (rawmode, 0, orientation))])
|
||||
ImageFile._save(
|
||||
im, fp, [("raw", (0, 0) + im.size, 0, (rawmode, 0, orientation))])
|
||||
|
||||
#
|
||||
# --------------------------------------------------------------------
|
||||
# Registry
|
||||
|
||||
Image.register_open("TGA", TgaImageFile, _accept)
|
||||
Image.register_open("TGA", TgaImageFile)
|
||||
Image.register_save("TGA", _save)
|
||||
|
||||
Image.register_extension("TGA", ".tga")
|
||||
|
|
|
@ -49,17 +49,18 @@ from PIL import _binary
|
|||
from PIL._util import isStringType
|
||||
|
||||
import warnings
|
||||
import array, sys
|
||||
import array
|
||||
import sys
|
||||
import collections
|
||||
import itertools
|
||||
import os
|
||||
|
||||
# Set these to true to force use of libtiff for reading or writing.
|
||||
READ_LIBTIFF = False
|
||||
WRITE_LIBTIFF= False
|
||||
WRITE_LIBTIFF = False
|
||||
|
||||
II = b"II" # little-endian (intel-style)
|
||||
MM = b"MM" # big-endian (motorola-style)
|
||||
II = b"II" # little-endian (Intel style)
|
||||
MM = b"MM" # big-endian (Motorola style)
|
||||
|
||||
i8 = _binary.i8
|
||||
o8 = _binary.o8
|
||||
|
@ -109,8 +110,8 @@ EXTRASAMPLES = 338
|
|||
SAMPLEFORMAT = 339
|
||||
JPEGTABLES = 347
|
||||
COPYRIGHT = 33432
|
||||
IPTC_NAA_CHUNK = 33723 # newsphoto properties
|
||||
PHOTOSHOP_CHUNK = 34377 # photoshop properties
|
||||
IPTC_NAA_CHUNK = 33723 # newsphoto properties
|
||||
PHOTOSHOP_CHUNK = 34377 # photoshop properties
|
||||
ICCPROFILE = 34675
|
||||
EXIFIFD = 34665
|
||||
XMP = 700
|
||||
|
@ -126,10 +127,10 @@ COMPRESSION_INFO = {
|
|||
3: "group3",
|
||||
4: "group4",
|
||||
5: "tiff_lzw",
|
||||
6: "tiff_jpeg", # obsolete
|
||||
6: "tiff_jpeg", # obsolete
|
||||
7: "jpeg",
|
||||
8: "tiff_adobe_deflate",
|
||||
32771: "tiff_raw_16", # 16-bit padding
|
||||
32771: "tiff_raw_16", # 16-bit padding
|
||||
32773: "packbits",
|
||||
32809: "tiff_thunderscan",
|
||||
32946: "tiff_deflate",
|
||||
|
@ -137,7 +138,7 @@ COMPRESSION_INFO = {
|
|||
34677: "tiff_sgilog24",
|
||||
}
|
||||
|
||||
COMPRESSION_INFO_REV = dict([(v,k) for (k,v) in COMPRESSION_INFO.items()])
|
||||
COMPRESSION_INFO_REV = dict([(v, k) for (k, v) in COMPRESSION_INFO.items()])
|
||||
|
||||
OPEN_INFO = {
|
||||
# (ByteOrder, PhotoInterpretation, SampleFormat, FillOrder, BitsPerSample,
|
||||
|
@ -150,7 +151,7 @@ OPEN_INFO = {
|
|||
(II, 1, 1, 1, (1,), ()): ("1", "1"),
|
||||
(II, 1, 1, 2, (1,), ()): ("1", "1;R"),
|
||||
(II, 1, 1, 1, (8,), ()): ("L", "L"),
|
||||
(II, 1, 1, 1, (8,8), (2,)): ("LA", "LA"),
|
||||
(II, 1, 1, 1, (8, 8), (2,)): ("LA", "LA"),
|
||||
(II, 1, 1, 2, (8,), ()): ("L", "L;R"),
|
||||
(II, 1, 1, 1, (12,), ()): ("I;16", "I;12"),
|
||||
(II, 1, 1, 1, (16,), ()): ("I;16", "I;16"),
|
||||
|
@ -158,13 +159,13 @@ OPEN_INFO = {
|
|||
(II, 1, 1, 1, (32,), ()): ("I", "I;32N"),
|
||||
(II, 1, 2, 1, (32,), ()): ("I", "I;32S"),
|
||||
(II, 1, 3, 1, (32,), ()): ("F", "F;32F"),
|
||||
(II, 2, 1, 1, (8,8,8), ()): ("RGB", "RGB"),
|
||||
(II, 2, 1, 2, (8,8,8), ()): ("RGB", "RGB;R"),
|
||||
(II, 2, 1, 1, (8,8,8,8), ()): ("RGBA", "RGBA"), # missing ExtraSamples
|
||||
(II, 2, 1, 1, (8,8,8,8), (0,)): ("RGBX", "RGBX"),
|
||||
(II, 2, 1, 1, (8,8,8,8), (1,)): ("RGBA", "RGBa"),
|
||||
(II, 2, 1, 1, (8,8,8,8), (2,)): ("RGBA", "RGBA"),
|
||||
(II, 2, 1, 1, (8,8,8,8), (999,)): ("RGBA", "RGBA"), # corel draw 10
|
||||
(II, 2, 1, 1, (8, 8, 8), ()): ("RGB", "RGB"),
|
||||
(II, 2, 1, 2, (8, 8, 8), ()): ("RGB", "RGB;R"),
|
||||
(II, 2, 1, 1, (8, 8, 8, 8), ()): ("RGBA", "RGBA"), # missing ExtraSamples
|
||||
(II, 2, 1, 1, (8, 8, 8, 8), (0,)): ("RGBX", "RGBX"),
|
||||
(II, 2, 1, 1, (8, 8, 8, 8), (1,)): ("RGBA", "RGBa"),
|
||||
(II, 2, 1, 1, (8, 8, 8, 8), (2,)): ("RGBA", "RGBA"),
|
||||
(II, 2, 1, 1, (8, 8, 8, 8), (999,)): ("RGBA", "RGBA"), # Corel Draw 10
|
||||
(II, 3, 1, 1, (1,), ()): ("P", "P;1"),
|
||||
(II, 3, 1, 2, (1,), ()): ("P", "P;1R"),
|
||||
(II, 3, 1, 1, (2,), ()): ("P", "P;2"),
|
||||
|
@ -172,11 +173,11 @@ OPEN_INFO = {
|
|||
(II, 3, 1, 1, (4,), ()): ("P", "P;4"),
|
||||
(II, 3, 1, 2, (4,), ()): ("P", "P;4R"),
|
||||
(II, 3, 1, 1, (8,), ()): ("P", "P"),
|
||||
(II, 3, 1, 1, (8,8), (2,)): ("PA", "PA"),
|
||||
(II, 3, 1, 1, (8, 8), (2,)): ("PA", "PA"),
|
||||
(II, 3, 1, 2, (8,), ()): ("P", "P;R"),
|
||||
(II, 5, 1, 1, (8,8,8,8), ()): ("CMYK", "CMYK"),
|
||||
(II, 6, 1, 1, (8,8,8), ()): ("YCbCr", "YCbCr"),
|
||||
(II, 8, 1, 1, (8,8,8), ()): ("LAB", "LAB"),
|
||||
(II, 5, 1, 1, (8, 8, 8, 8), ()): ("CMYK", "CMYK"),
|
||||
(II, 6, 1, 1, (8, 8, 8), ()): ("YCbCr", "YCbCr"),
|
||||
(II, 8, 1, 1, (8, 8, 8), ()): ("LAB", "LAB"),
|
||||
|
||||
(MM, 0, 1, 1, (1,), ()): ("1", "1;I"),
|
||||
(MM, 0, 1, 2, (1,), ()): ("1", "1;IR"),
|
||||
|
@ -185,18 +186,18 @@ OPEN_INFO = {
|
|||
(MM, 1, 1, 1, (1,), ()): ("1", "1"),
|
||||
(MM, 1, 1, 2, (1,), ()): ("1", "1;R"),
|
||||
(MM, 1, 1, 1, (8,), ()): ("L", "L"),
|
||||
(MM, 1, 1, 1, (8,8), (2,)): ("LA", "LA"),
|
||||
(MM, 1, 1, 1, (8, 8), (2,)): ("LA", "LA"),
|
||||
(MM, 1, 1, 2, (8,), ()): ("L", "L;R"),
|
||||
(MM, 1, 1, 1, (16,), ()): ("I;16B", "I;16B"),
|
||||
(MM, 1, 2, 1, (16,), ()): ("I;16BS", "I;16BS"),
|
||||
(MM, 1, 2, 1, (32,), ()): ("I;32BS", "I;32BS"),
|
||||
(MM, 1, 3, 1, (32,), ()): ("F", "F;32BF"),
|
||||
(MM, 2, 1, 1, (8,8,8), ()): ("RGB", "RGB"),
|
||||
(MM, 2, 1, 2, (8,8,8), ()): ("RGB", "RGB;R"),
|
||||
(MM, 2, 1, 1, (8,8,8,8), (0,)): ("RGBX", "RGBX"),
|
||||
(MM, 2, 1, 1, (8,8,8,8), (1,)): ("RGBA", "RGBa"),
|
||||
(MM, 2, 1, 1, (8,8,8,8), (2,)): ("RGBA", "RGBA"),
|
||||
(MM, 2, 1, 1, (8,8,8,8), (999,)): ("RGBA", "RGBA"), # corel draw 10
|
||||
(MM, 2, 1, 1, (8, 8, 8), ()): ("RGB", "RGB"),
|
||||
(MM, 2, 1, 2, (8, 8, 8), ()): ("RGB", "RGB;R"),
|
||||
(MM, 2, 1, 1, (8, 8, 8, 8), (0,)): ("RGBX", "RGBX"),
|
||||
(MM, 2, 1, 1, (8, 8, 8, 8), (1,)): ("RGBA", "RGBa"),
|
||||
(MM, 2, 1, 1, (8, 8, 8, 8), (2,)): ("RGBA", "RGBA"),
|
||||
(MM, 2, 1, 1, (8, 8, 8, 8), (999,)): ("RGBA", "RGBA"), # Corel Draw 10
|
||||
(MM, 3, 1, 1, (1,), ()): ("P", "P;1"),
|
||||
(MM, 3, 1, 2, (1,), ()): ("P", "P;1R"),
|
||||
(MM, 3, 1, 1, (2,), ()): ("P", "P;2"),
|
||||
|
@ -204,19 +205,21 @@ OPEN_INFO = {
|
|||
(MM, 3, 1, 1, (4,), ()): ("P", "P;4"),
|
||||
(MM, 3, 1, 2, (4,), ()): ("P", "P;4R"),
|
||||
(MM, 3, 1, 1, (8,), ()): ("P", "P"),
|
||||
(MM, 3, 1, 1, (8,8), (2,)): ("PA", "PA"),
|
||||
(MM, 3, 1, 1, (8, 8), (2,)): ("PA", "PA"),
|
||||
(MM, 3, 1, 2, (8,), ()): ("P", "P;R"),
|
||||
(MM, 5, 1, 1, (8,8,8,8), ()): ("CMYK", "CMYK"),
|
||||
(MM, 6, 1, 1, (8,8,8), ()): ("YCbCr", "YCbCr"),
|
||||
(MM, 8, 1, 1, (8,8,8), ()): ("LAB", "LAB"),
|
||||
(MM, 5, 1, 1, (8, 8, 8, 8), ()): ("CMYK", "CMYK"),
|
||||
(MM, 6, 1, 1, (8, 8, 8), ()): ("YCbCr", "YCbCr"),
|
||||
(MM, 8, 1, 1, (8, 8, 8), ()): ("LAB", "LAB"),
|
||||
|
||||
}
|
||||
|
||||
PREFIXES = [b"MM\000\052", b"II\052\000", b"II\xBC\000"]
|
||||
|
||||
|
||||
def _accept(prefix):
|
||||
return prefix[:4] in PREFIXES
|
||||
|
||||
|
||||
##
|
||||
# Wrapper for TIFF IFDs.
|
||||
|
||||
|
@ -276,8 +279,9 @@ class ImageFileDirectory(collections.MutableMapping):
|
|||
#: For a complete dictionary, use the as_dict method.
|
||||
self.tags = {}
|
||||
self.tagdata = {}
|
||||
self.tagtype = {} # added 2008-06-05 by Florian Hoech
|
||||
self.tagtype = {} # added 2008-06-05 by Florian Hoech
|
||||
self.next = None
|
||||
self.offset = None
|
||||
|
||||
def __str__(self):
|
||||
return str(self.as_dict())
|
||||
|
@ -287,7 +291,9 @@ class ImageFileDirectory(collections.MutableMapping):
|
|||
return dict(self.items())
|
||||
|
||||
def named(self):
|
||||
"""Returns the complete tag dictionary, with named tags where posible."""
|
||||
"""
|
||||
Returns the complete tag dictionary, with named tags where posible.
|
||||
"""
|
||||
from PIL import TiffTags
|
||||
result = {}
|
||||
for tag_code, value in self.items():
|
||||
|
@ -295,7 +301,6 @@ class ImageFileDirectory(collections.MutableMapping):
|
|||
result[tag_name] = value
|
||||
return result
|
||||
|
||||
|
||||
# dictionary API
|
||||
|
||||
def __len__(self):
|
||||
|
@ -305,7 +310,7 @@ class ImageFileDirectory(collections.MutableMapping):
|
|||
try:
|
||||
return self.tags[tag]
|
||||
except KeyError:
|
||||
data = self.tagdata[tag] # unpack on the fly
|
||||
data = self.tagdata[tag] # unpack on the fly
|
||||
type = self.tagtype[tag]
|
||||
size, handler = self.load_dispatch[type]
|
||||
self.tags[tag] = data = handler(self, data)
|
||||
|
@ -319,7 +324,7 @@ class ImageFileDirectory(collections.MutableMapping):
|
|||
if tag == SAMPLEFORMAT:
|
||||
# work around broken (?) matrox library
|
||||
# (from Ted Wright, via Bob Klimek)
|
||||
raise KeyError # use default
|
||||
raise KeyError # use default
|
||||
raise ValueError("not a scalar")
|
||||
return value[0]
|
||||
except KeyError:
|
||||
|
@ -411,6 +416,7 @@ class ImageFileDirectory(collections.MutableMapping):
|
|||
# load tag dictionary
|
||||
|
||||
self.reset()
|
||||
self.offset = fp.tell()
|
||||
|
||||
i16 = self.i16
|
||||
i32 = self.i32
|
||||
|
@ -433,7 +439,7 @@ class ImageFileDirectory(collections.MutableMapping):
|
|||
except KeyError:
|
||||
if Image.DEBUG:
|
||||
print("- unsupported type", typ)
|
||||
continue # ignore unsupported type
|
||||
continue # ignore unsupported type
|
||||
|
||||
size, handler = dispatch
|
||||
|
||||
|
@ -442,21 +448,28 @@ class ImageFileDirectory(collections.MutableMapping):
|
|||
# Get and expand tag value
|
||||
if size > 4:
|
||||
here = fp.tell()
|
||||
if Image.DEBUG:
|
||||
print ("Tag Location: %s" %here)
|
||||
fp.seek(i32(ifd, 8))
|
||||
if Image.DEBUG:
|
||||
print ("Data Location: %s" %fp.tell())
|
||||
data = ImageFile._safe_read(fp, size)
|
||||
fp.seek(here)
|
||||
else:
|
||||
data = ifd[8:8+size]
|
||||
|
||||
if len(data) != size:
|
||||
warnings.warn("Possibly corrupt EXIF data. Expecting to read %d bytes but only got %d. Skipping tag %s" % (size, len(data), tag))
|
||||
warnings.warn("Possibly corrupt EXIF data. "
|
||||
"Expecting to read %d bytes but only got %d. "
|
||||
"Skipping tag %s" % (size, len(data), tag))
|
||||
continue
|
||||
|
||||
self.tagdata[tag] = data
|
||||
self.tagtype[tag] = typ
|
||||
|
||||
if Image.DEBUG:
|
||||
if tag in (COLORMAP, IPTC_NAA_CHUNK, PHOTOSHOP_CHUNK, ICCPROFILE, XMP):
|
||||
if tag in (COLORMAP, IPTC_NAA_CHUNK, PHOTOSHOP_CHUNK,
|
||||
ICCPROFILE, XMP):
|
||||
print("- value: <table: %d bytes>" % size)
|
||||
else:
|
||||
print("- value:", self[tag])
|
||||
|
@ -518,8 +531,8 @@ class ImageFileDirectory(collections.MutableMapping):
|
|||
# integer data
|
||||
if tag == STRIPOFFSETS:
|
||||
stripoffsets = len(directory)
|
||||
typ = 4 # to avoid catch-22
|
||||
elif tag in (X_RESOLUTION, Y_RESOLUTION) or typ==5:
|
||||
typ = 4 # to avoid catch-22
|
||||
elif tag in (X_RESOLUTION, Y_RESOLUTION) or typ == 5:
|
||||
# identify rational data fields
|
||||
typ = 5
|
||||
if isinstance(value[0], tuple):
|
||||
|
@ -541,7 +554,8 @@ class ImageFileDirectory(collections.MutableMapping):
|
|||
typname = TiffTags.TYPES.get(typ, "unknown")
|
||||
print("save: %s (%d)" % (tagname, tag), end=' ')
|
||||
print("- type: %s (%d)" % (typname, typ), end=' ')
|
||||
if tag in (COLORMAP, IPTC_NAA_CHUNK, PHOTOSHOP_CHUNK, ICCPROFILE, XMP):
|
||||
if tag in (COLORMAP, IPTC_NAA_CHUNK, PHOTOSHOP_CHUNK,
|
||||
ICCPROFILE, XMP):
|
||||
size = len(data)
|
||||
print("- value: <table: %d bytes>" % size)
|
||||
else:
|
||||
|
@ -576,7 +590,7 @@ class ImageFileDirectory(collections.MutableMapping):
|
|||
fp.write(o16(tag) + o16(typ) + o32(count) + value)
|
||||
|
||||
# -- overwrite here for multi-page --
|
||||
fp.write(b"\0\0\0\0") # end of directory
|
||||
fp.write(b"\0\0\0\0") # end of directory
|
||||
|
||||
# pass 3: write auxiliary data to file
|
||||
for tag, typ, count, value, data in directory:
|
||||
|
@ -586,6 +600,7 @@ class ImageFileDirectory(collections.MutableMapping):
|
|||
|
||||
return offset
|
||||
|
||||
|
||||
##
|
||||
# Image plugin for TIFF files.
|
||||
|
||||
|
@ -616,23 +631,25 @@ class TiffImageFile(ImageFile.ImageFile):
|
|||
print ("- __first:", self.__first)
|
||||
print ("- ifh: ", ifh)
|
||||
|
||||
# and load the first frame
|
||||
# and load the first frame
|
||||
self._seek(0)
|
||||
|
||||
def seek(self, frame):
|
||||
"Select a given frame as current image"
|
||||
|
||||
if frame < 0:
|
||||
frame = 0
|
||||
self._seek(frame)
|
||||
# Create a new core image object on second and
|
||||
# subsequent frames in the image. Image may be
|
||||
# different size/mode.
|
||||
Image._decompression_bomb_check(self.size)
|
||||
self.im = Image.core.new(self.mode, self.size)
|
||||
|
||||
def tell(self):
|
||||
"Return the current frame number"
|
||||
|
||||
return self._tell()
|
||||
|
||||
def _seek(self, frame):
|
||||
|
||||
self.fp = self.__fp
|
||||
if frame < self.__frame:
|
||||
# rewind file
|
||||
|
@ -641,14 +658,21 @@ class TiffImageFile(ImageFile.ImageFile):
|
|||
while self.__frame < frame:
|
||||
if not self.__next:
|
||||
raise EOFError("no more images in TIFF file")
|
||||
if Image.DEBUG:
|
||||
print("Seeking to frame %s, on frame %s, __next %s, location: %s"%
|
||||
(frame, self.__frame, self.__next, self.fp.tell()))
|
||||
# reset python3 buffered io handle in case fp
|
||||
# was passed to libtiff, invalidating the buffer
|
||||
self.fp.tell()
|
||||
self.fp.seek(self.__next)
|
||||
if Image.DEBUG:
|
||||
print("Loading tags, location: %s"%self.fp.tell())
|
||||
self.tag.load(self.fp)
|
||||
self.__next = self.tag.next
|
||||
self.__frame += 1
|
||||
self._setup()
|
||||
|
||||
|
||||
def _tell(self):
|
||||
|
||||
return self.__frame
|
||||
|
||||
def _decoder(self, rawmode, layer, tile=None):
|
||||
|
@ -694,9 +718,12 @@ class TiffImageFile(ImageFile.ImageFile):
|
|||
if not len(self.tile) == 1:
|
||||
raise IOError("Not exactly one tile")
|
||||
|
||||
# (self._compression, (extents tuple), 0, (rawmode, self._compression, fp))
|
||||
# (self._compression, (extents tuple),
|
||||
# 0, (rawmode, self._compression, fp))
|
||||
ignored, extents, ignored_2, args = self.tile[0]
|
||||
decoder = Image._getdecoder(self.mode, 'libtiff', args, self.decoderconfig)
|
||||
args = args + (self.ifd.offset,)
|
||||
decoder = Image._getdecoder(self.mode, 'libtiff', args,
|
||||
self.decoderconfig)
|
||||
try:
|
||||
decoder.setimage(self.im, extents)
|
||||
except ValueError:
|
||||
|
@ -706,35 +733,36 @@ class TiffImageFile(ImageFile.ImageFile):
|
|||
# We've got a stringio like thing passed in. Yay for all in memory.
|
||||
# The decoder needs the entire file in one shot, so there's not
|
||||
# a lot we can do here other than give it the entire file.
|
||||
# unless we could do something like get the address of the underlying
|
||||
# string for stringio.
|
||||
# unless we could do something like get the address of the
|
||||
# underlying string for stringio.
|
||||
#
|
||||
# Rearranging for supporting byteio items, since they have a fileno
|
||||
# that returns an IOError if there's no underlying fp. Easier to deal
|
||||
# with here by reordering.
|
||||
# that returns an IOError if there's no underlying fp. Easier to
|
||||
# dea. with here by reordering.
|
||||
if Image.DEBUG:
|
||||
print ("have getvalue. just sending in a string from getvalue")
|
||||
n,err = decoder.decode(self.fp.getvalue())
|
||||
n, err = decoder.decode(self.fp.getvalue())
|
||||
elif hasattr(self.fp, "fileno"):
|
||||
# we've got a actual file on disk, pass in the fp.
|
||||
if Image.DEBUG:
|
||||
print ("have fileno, calling fileno version of the decoder.")
|
||||
self.fp.seek(0)
|
||||
n,err = decoder.decode(b"fpfp") # 4 bytes, otherwise the trace might error out
|
||||
# 4 bytes, otherwise the trace might error out
|
||||
n, err = decoder.decode(b"fpfp")
|
||||
else:
|
||||
# we have something else.
|
||||
if Image.DEBUG:
|
||||
print ("don't have fileno or getvalue. just reading")
|
||||
# UNDONE -- so much for that buffer size thing.
|
||||
n,err = decoder.decode(self.fp.read())
|
||||
|
||||
n, err = decoder.decode(self.fp.read())
|
||||
|
||||
self.tile = []
|
||||
self.readonly = 0
|
||||
# libtiff closed the fp in a, we need to close self.fp, if possible
|
||||
if hasattr(self.fp, 'close'):
|
||||
self.fp.close()
|
||||
self.fp = None # might be shared
|
||||
if not self.__next:
|
||||
self.fp.close()
|
||||
self.fp = None # might be shared
|
||||
|
||||
if err < 0:
|
||||
raise IOError(err)
|
||||
|
@ -810,11 +838,11 @@ class TiffImageFile(ImageFile.ImageFile):
|
|||
xres = xres[0] / (xres[1] or 1)
|
||||
yres = yres[0] / (yres[1] or 1)
|
||||
resunit = getscalar(RESOLUTION_UNIT, 1)
|
||||
if resunit == 2: # dots per inch
|
||||
if resunit == 2: # dots per inch
|
||||
self.info["dpi"] = xres, yres
|
||||
elif resunit == 3: # dots per centimeter. convert to dpi
|
||||
elif resunit == 3: # dots per centimeter. convert to dpi
|
||||
self.info["dpi"] = xres * 2.54, yres * 2.54
|
||||
else: # No absolute unit of measurement
|
||||
else: # No absolute unit of measurement
|
||||
self.info["resolution"] = xres, yres
|
||||
|
||||
# build tile descriptors
|
||||
|
@ -825,13 +853,16 @@ class TiffImageFile(ImageFile.ImageFile):
|
|||
offsets = self.tag[STRIPOFFSETS]
|
||||
h = getscalar(ROWSPERSTRIP, ysize)
|
||||
w = self.size[0]
|
||||
if READ_LIBTIFF or self._compression in ["tiff_ccitt", "group3", "group4",
|
||||
"tiff_jpeg", "tiff_adobe_deflate",
|
||||
"tiff_thunderscan", "tiff_deflate",
|
||||
"tiff_sgilog", "tiff_sgilog24",
|
||||
if READ_LIBTIFF or self._compression in ["tiff_ccitt", "group3",
|
||||
"group4", "tiff_jpeg",
|
||||
"tiff_adobe_deflate",
|
||||
"tiff_thunderscan",
|
||||
"tiff_deflate",
|
||||
"tiff_sgilog",
|
||||
"tiff_sgilog24",
|
||||
"tiff_raw_16"]:
|
||||
## if Image.DEBUG:
|
||||
## print "Activating g4 compression for whole file"
|
||||
# if Image.DEBUG:
|
||||
# print "Activating g4 compression for whole file"
|
||||
|
||||
# Decoder expects entire file as one tile.
|
||||
# There's a buffer size limit in load (64k)
|
||||
|
@ -850,7 +881,8 @@ class TiffImageFile(ImageFile.ImageFile):
|
|||
|
||||
# libtiff closes the file descriptor, so pass in a dup.
|
||||
try:
|
||||
fp = hasattr(self.fp, "fileno") and os.dup(self.fp.fileno())
|
||||
fp = hasattr(self.fp, "fileno") and \
|
||||
os.dup(self.fp.fileno())
|
||||
except IOError:
|
||||
# io.BytesIO have a fileno, but returns an IOError if
|
||||
# it doesn't use a file descriptor.
|
||||
|
@ -881,7 +913,7 @@ class TiffImageFile(ImageFile.ImageFile):
|
|||
|
||||
# Offset in the tile tuple is 0, we go from 0,0 to
|
||||
# w,h, and we only do this once -- eds
|
||||
a = (rawmode, self._compression, fp )
|
||||
a = (rawmode, self._compression, fp)
|
||||
self.tile.append(
|
||||
(self._compression,
|
||||
(0, 0, w, ysize),
|
||||
|
@ -893,8 +925,8 @@ class TiffImageFile(ImageFile.ImageFile):
|
|||
a = self._decoder(rawmode, l, i)
|
||||
self.tile.append(
|
||||
(self._compression,
|
||||
(0, min(y, ysize), w, min(y+h, ysize)),
|
||||
offsets[i], a))
|
||||
(0, min(y, ysize), w, min(y+h, ysize)),
|
||||
offsets[i], a))
|
||||
if Image.DEBUG:
|
||||
print ("tiles: ", self.tile)
|
||||
y = y + h
|
||||
|
@ -914,8 +946,8 @@ class TiffImageFile(ImageFile.ImageFile):
|
|||
# is not a multiple of the tile size...
|
||||
self.tile.append(
|
||||
(self._compression,
|
||||
(x, y, x+w, y+h),
|
||||
o, a))
|
||||
(x, y, x+w, y+h),
|
||||
o, a))
|
||||
x = x + w
|
||||
if x >= self.size[0]:
|
||||
x, y = 0, y + h
|
||||
|
@ -937,25 +969,27 @@ class TiffImageFile(ImageFile.ImageFile):
|
|||
# --------------------------------------------------------------------
|
||||
# Write TIFF files
|
||||
|
||||
# little endian is default except for image modes with explict big endian byte-order
|
||||
# little endian is default except for image modes with
|
||||
# explict big endian byte-order
|
||||
|
||||
SAVE_INFO = {
|
||||
# mode => rawmode, byteorder, photometrics, sampleformat, bitspersample, extra
|
||||
# mode => rawmode, byteorder, photometrics,
|
||||
# sampleformat, bitspersample, extra
|
||||
"1": ("1", II, 1, 1, (1,), None),
|
||||
"L": ("L", II, 1, 1, (8,), None),
|
||||
"LA": ("LA", II, 1, 1, (8,8), 2),
|
||||
"LA": ("LA", II, 1, 1, (8, 8), 2),
|
||||
"P": ("P", II, 3, 1, (8,), None),
|
||||
"PA": ("PA", II, 3, 1, (8,8), 2),
|
||||
"PA": ("PA", II, 3, 1, (8, 8), 2),
|
||||
"I": ("I;32S", II, 1, 2, (32,), None),
|
||||
"I;16": ("I;16", II, 1, 1, (16,), None),
|
||||
"I;16S": ("I;16S", II, 1, 2, (16,), None),
|
||||
"F": ("F;32F", II, 1, 3, (32,), None),
|
||||
"RGB": ("RGB", II, 2, 1, (8,8,8), None),
|
||||
"RGBX": ("RGBX", II, 2, 1, (8,8,8,8), 0),
|
||||
"RGBA": ("RGBA", II, 2, 1, (8,8,8,8), 2),
|
||||
"CMYK": ("CMYK", II, 5, 1, (8,8,8,8), None),
|
||||
"YCbCr": ("YCbCr", II, 6, 1, (8,8,8), None),
|
||||
"LAB": ("LAB", II, 8, 1, (8,8,8), None),
|
||||
"RGB": ("RGB", II, 2, 1, (8, 8, 8), None),
|
||||
"RGBX": ("RGBX", II, 2, 1, (8, 8, 8, 8), 0),
|
||||
"RGBA": ("RGBA", II, 2, 1, (8, 8, 8, 8), 2),
|
||||
"CMYK": ("CMYK", II, 5, 1, (8, 8, 8, 8), None),
|
||||
"YCbCr": ("YCbCr", II, 6, 1, (8, 8, 8), None),
|
||||
"LAB": ("LAB", II, 8, 1, (8, 8, 8), None),
|
||||
|
||||
"I;32BS": ("I;32BS", MM, 1, 2, (32,), None),
|
||||
"I;16B": ("I;16B", MM, 1, 1, (16,), None),
|
||||
|
@ -963,6 +997,7 @@ SAVE_INFO = {
|
|||
"F;32BF": ("F;32BF", MM, 1, 3, (32,), None),
|
||||
}
|
||||
|
||||
|
||||
def _cvt_res(value):
|
||||
# convert value to TIFF rational number -- (numerator, denominator)
|
||||
if isinstance(value, collections.Sequence):
|
||||
|
@ -973,6 +1008,7 @@ def _cvt_res(value):
|
|||
value = float(value)
|
||||
return (int(value * 65536), 65536)
|
||||
|
||||
|
||||
def _save(im, fp, filename):
|
||||
|
||||
try:
|
||||
|
@ -982,7 +1018,8 @@ def _save(im, fp, filename):
|
|||
|
||||
ifd = ImageFileDirectory(prefix)
|
||||
|
||||
compression = im.encoderinfo.get('compression',im.info.get('compression','raw'))
|
||||
compression = im.encoderinfo.get('compression', im.info.get('compression',
|
||||
'raw'))
|
||||
|
||||
libtiff = WRITE_LIBTIFF or compression != 'raw'
|
||||
|
||||
|
@ -999,17 +1036,16 @@ def _save(im, fp, filename):
|
|||
ifd[IMAGELENGTH] = im.size[1]
|
||||
|
||||
# write any arbitrary tags passed in as an ImageFileDirectory
|
||||
info = im.encoderinfo.get("tiffinfo",{})
|
||||
info = im.encoderinfo.get("tiffinfo", {})
|
||||
if Image.DEBUG:
|
||||
print ("Tiffinfo Keys: %s"% info.keys)
|
||||
print("Tiffinfo Keys: %s" % info.keys)
|
||||
keys = list(info.keys())
|
||||
for key in keys:
|
||||
ifd[key] = info.get(key)
|
||||
try:
|
||||
ifd.tagtype[key] = info.tagtype[key]
|
||||
except:
|
||||
pass # might not be an IFD, Might not have populated type
|
||||
|
||||
pass # might not be an IFD, Might not have populated type
|
||||
|
||||
# additions written by Greg Couch, gregc@cgl.ucsf.edu
|
||||
# inspired by image-sig posting from Kevin Cazabon, kcazabon@home.com
|
||||
|
@ -1030,7 +1066,7 @@ def _save(im, fp, filename):
|
|||
ifd[IMAGEDESCRIPTION] = im.encoderinfo["description"]
|
||||
if "resolution" in im.encoderinfo:
|
||||
ifd[X_RESOLUTION] = ifd[Y_RESOLUTION] \
|
||||
= _cvt_res(im.encoderinfo["resolution"])
|
||||
= _cvt_res(im.encoderinfo["resolution"])
|
||||
if "x resolution" in im.encoderinfo:
|
||||
ifd[X_RESOLUTION] = _cvt_res(im.encoderinfo["x resolution"])
|
||||
if "y resolution" in im.encoderinfo:
|
||||
|
@ -1077,8 +1113,9 @@ def _save(im, fp, filename):
|
|||
stride = len(bits) * ((im.size[0]*bits[0]+7)//8)
|
||||
ifd[ROWSPERSTRIP] = im.size[1]
|
||||
ifd[STRIPBYTECOUNTS] = stride * im.size[1]
|
||||
ifd[STRIPOFFSETS] = 0 # this is adjusted by IFD writer
|
||||
ifd[COMPRESSION] = COMPRESSION_INFO_REV.get(compression,1) # no compression by default
|
||||
ifd[STRIPOFFSETS] = 0 # this is adjusted by IFD writer
|
||||
# no compression by default:
|
||||
ifd[COMPRESSION] = COMPRESSION_INFO_REV.get(compression, 1)
|
||||
|
||||
if libtiff:
|
||||
if Image.DEBUG:
|
||||
|
@ -1089,23 +1126,27 @@ def _save(im, fp, filename):
|
|||
fp.seek(0)
|
||||
_fp = os.dup(fp.fileno())
|
||||
|
||||
blocklist = [STRIPOFFSETS, STRIPBYTECOUNTS, ROWSPERSTRIP, ICCPROFILE] # ICC Profile crashes.
|
||||
atts={}
|
||||
# ICC Profile crashes.
|
||||
blocklist = [STRIPOFFSETS, STRIPBYTECOUNTS, ROWSPERSTRIP, ICCPROFILE]
|
||||
atts = {}
|
||||
# bits per sample is a single short in the tiff directory, not a list.
|
||||
atts[BITSPERSAMPLE] = bits[0]
|
||||
# Merge the ones that we have with (optional) more bits from
|
||||
# the original file, e.g x,y resolution so that we can
|
||||
# save(load('')) == original file.
|
||||
for k,v in itertools.chain(ifd.items(), getattr(im, 'ifd', {}).items()):
|
||||
for k, v in itertools.chain(ifd.items(),
|
||||
getattr(im, 'ifd', {}).items()):
|
||||
if k not in atts and k not in blocklist:
|
||||
if type(v[0]) == tuple and len(v) > 1:
|
||||
# A tuple of more than one rational tuples
|
||||
# flatten to floats, following tiffcp.c->cpTag->TIFF_RATIONAL
|
||||
# flatten to floats,
|
||||
# following tiffcp.c->cpTag->TIFF_RATIONAL
|
||||
atts[k] = [float(elt[0])/float(elt[1]) for elt in v]
|
||||
continue
|
||||
if type(v[0]) == tuple and len(v) == 1:
|
||||
# A tuple of one rational tuples
|
||||
# flatten to floats, following tiffcp.c->cpTag->TIFF_RATIONAL
|
||||
# flatten to floats,
|
||||
# following tiffcp.c->cpTag->TIFF_RATIONAL
|
||||
atts[k] = float(v[0][0])/float(v[0][1])
|
||||
continue
|
||||
if type(v) == tuple and len(v) > 2:
|
||||
|
@ -1115,7 +1156,8 @@ def _save(im, fp, filename):
|
|||
continue
|
||||
if type(v) == tuple and len(v) == 2:
|
||||
# one rational tuple
|
||||
# flatten to float, following tiffcp.c->cpTag->TIFF_RATIONAL
|
||||
# flatten to float,
|
||||
# following tiffcp.c->cpTag->TIFF_RATIONAL
|
||||
atts[k] = float(v[0])/float(v[1])
|
||||
continue
|
||||
if type(v) == tuple and len(v) == 1:
|
||||
|
@ -1141,9 +1183,10 @@ def _save(im, fp, filename):
|
|||
a = (rawmode, compression, _fp, filename, atts)
|
||||
# print (im.mode, compression, a, im.encoderconfig)
|
||||
e = Image._getencoder(im.mode, 'libtiff', a, im.encoderconfig)
|
||||
e.setimage(im.im, (0,0)+im.size)
|
||||
e.setimage(im.im, (0, 0)+im.size)
|
||||
while True:
|
||||
l, s, d = e.encode(16*1024) # undone, change to self.decodermaxblock
|
||||
# undone, change to self.decodermaxblock:
|
||||
l, s, d = e.encode(16*1024)
|
||||
if not _fp:
|
||||
fp.write(d)
|
||||
if s:
|
||||
|
@ -1155,13 +1198,12 @@ def _save(im, fp, filename):
|
|||
offset = ifd.save(fp)
|
||||
|
||||
ImageFile._save(im, fp, [
|
||||
("raw", (0,0)+im.size, offset, (rawmode, stride, 1))
|
||||
("raw", (0, 0)+im.size, offset, (rawmode, stride, 1))
|
||||
])
|
||||
|
||||
|
||||
# -- helper for multi-page save --
|
||||
if "_debug_multipage" in im.encoderinfo:
|
||||
#just to access o32 and o16 (using correct byte order)
|
||||
# just to access o32 and o16 (using correct byte order)
|
||||
im._debug_multipage = ifd
|
||||
|
||||
#
|
||||
|
|
108
PIL/TiffTags.py
|
@ -46,8 +46,8 @@ TAGS = {
|
|||
(262, 5): "CMYK",
|
||||
(262, 6): "YCbCr",
|
||||
(262, 8): "CieLAB",
|
||||
(262, 32803): "CFA", # TIFF/EP, Adobe DNG
|
||||
(262, 32892): "LinearRaw", # Adobe DNG
|
||||
(262, 32803): "CFA", # TIFF/EP, Adobe DNG
|
||||
(262, 32892): "LinearRaw", # Adobe DNG
|
||||
|
||||
263: "Thresholding",
|
||||
264: "CellWidth",
|
||||
|
@ -147,6 +147,100 @@ TAGS = {
|
|||
# ICC Profile
|
||||
34675: "ICCProfile",
|
||||
|
||||
# Additional Exif Info
|
||||
33434: "ExposureTime",
|
||||
33437: "FNumber",
|
||||
34850: "ExposureProgram",
|
||||
34852: "SpectralSensitivity",
|
||||
34853: "GPSInfoIFD",
|
||||
34855: "ISOSpeedRatings",
|
||||
34856: "OECF",
|
||||
34864: "SensitivityType",
|
||||
34865: "StandardOutputSensitivity",
|
||||
34866: "RecommendedExposureIndex",
|
||||
34867: "ISOSpeed",
|
||||
34868: "ISOSpeedLatitudeyyy",
|
||||
34869: "ISOSpeedLatitudezzz",
|
||||
36864: "ExifVersion",
|
||||
36867: "DateTimeOriginal",
|
||||
36868: "DateTImeDigitized",
|
||||
37121: "ComponentsConfiguration",
|
||||
37122: "CompressedBitsPerPixel",
|
||||
37377: "ShutterSpeedValue",
|
||||
37378: "ApertureValue",
|
||||
37379: "BrightnessValue",
|
||||
37380: "ExposureBiasValue",
|
||||
37381: "MaxApertureValue",
|
||||
37382: "SubjectDistance",
|
||||
37383: "MeteringMode",
|
||||
37384: "LightSource",
|
||||
37385: "Flash",
|
||||
37386: "FocalLength",
|
||||
37396: "SubjectArea",
|
||||
37500: "MakerNote",
|
||||
37510: "UserComment",
|
||||
37520: "SubSec",
|
||||
37521: "SubSecTimeOriginal",
|
||||
37522: "SubsecTimeDigitized",
|
||||
40960: "FlashPixVersion",
|
||||
40961: "ColorSpace",
|
||||
40962: "PixelXDimension",
|
||||
40963: "PixelYDimension",
|
||||
40964: "RelatedSoundFile",
|
||||
40965: "InteroperabilityIFD",
|
||||
41483: "FlashEnergy",
|
||||
41484: "SpatialFrequencyResponse",
|
||||
41486: "FocalPlaneXResolution",
|
||||
41487: "FocalPlaneYResolution",
|
||||
41488: "FocalPlaneResolutionUnit",
|
||||
41492: "SubjectLocation",
|
||||
41493: "ExposureIndex",
|
||||
41495: "SensingMethod",
|
||||
41728: "FileSource",
|
||||
41729: "SceneType",
|
||||
41730: "CFAPattern",
|
||||
41985: "CustomRendered",
|
||||
41986: "ExposureMode",
|
||||
41987: "WhiteBalance",
|
||||
41988: "DigitalZoomRatio",
|
||||
41989: "FocalLengthIn35mmFilm",
|
||||
41990: "SceneCaptureType",
|
||||
41991: "GainControl",
|
||||
41992: "Contrast",
|
||||
41993: "Saturation",
|
||||
41994: "Sharpness",
|
||||
41995: "DeviceSettingDescription",
|
||||
41996: "SubjectDistanceRange",
|
||||
42016: "ImageUniqueID",
|
||||
42032: "CameraOwnerName",
|
||||
42033: "BodySerialNumber",
|
||||
42034: "LensSpecification",
|
||||
42035: "LensMake",
|
||||
42036: "LensModel",
|
||||
42037: "LensSerialNumber",
|
||||
42240: "Gamma",
|
||||
|
||||
# MP Info
|
||||
45056: "MPFVersion",
|
||||
45057: "NumberOfImages",
|
||||
45058: "MPEntry",
|
||||
45059: "ImageUIDList",
|
||||
45060: "TotalFrames",
|
||||
45313: "MPIndividualNum",
|
||||
45569: "PanOrientation",
|
||||
45570: "PanOverlap_H",
|
||||
45571: "PanOverlap_V",
|
||||
45572: "BaseViewpointNum",
|
||||
45573: "ConvergenceAngle",
|
||||
45574: "BaselineLength",
|
||||
45575: "VerticalDivergence",
|
||||
45576: "AxisDistance_X",
|
||||
45577: "AxisDistance_Y",
|
||||
45578: "AxisDistance_Z",
|
||||
45579: "YawAngle",
|
||||
45580: "PitchAngle",
|
||||
45581: "RollAngle",
|
||||
|
||||
# Adobe DNG
|
||||
50706: "DNGVersion",
|
||||
50707: "DNGBackwardVersion",
|
||||
|
@ -161,7 +255,6 @@ TAGS = {
|
|||
50716: "BlackLevelDeltaV",
|
||||
50717: "WhiteLevel",
|
||||
50718: "DefaultScale",
|
||||
50741: "BestQualityScale", # FIXME! Dictionary contains duplicate keys 50741
|
||||
50719: "DefaultCropOrigin",
|
||||
50720: "DefaultCropSize",
|
||||
50778: "CalibrationIlluminant1",
|
||||
|
@ -185,11 +278,12 @@ TAGS = {
|
|||
50737: "ChromaBlurRadius",
|
||||
50738: "AntiAliasStrength",
|
||||
50740: "DNGPrivateData",
|
||||
50741: "MakerNoteSafety", # FIXME! Dictionary contains duplicate keys 50741
|
||||
50741: "MakerNoteSafety",
|
||||
50780: "BestQualityScale",
|
||||
|
||||
#ImageJ
|
||||
50838: "ImageJMetaDataByteCounts", # private tag registered with Adobe
|
||||
50839: "ImageJMetaData", # private tag registered with Adobe
|
||||
# ImageJ
|
||||
50838: "ImageJMetaDataByteCounts", # private tag registered with Adobe
|
||||
50839: "ImageJMetaData", # private tag registered with Adobe
|
||||
}
|
||||
|
||||
##
|
||||
|
|
|
@ -29,6 +29,7 @@ xpm_head = re.compile(b"\"([0-9]*) ([0-9]*) ([0-9]*) ([0-9]*)")
|
|||
def _accept(prefix):
|
||||
return prefix[:9] == b"/* XPM */"
|
||||
|
||||
|
||||
##
|
||||
# Image plugin for X11 pixel maps.
|
||||
|
||||
|
@ -86,9 +87,9 @@ class XpmImageFile(ImageFile.ImageFile):
|
|||
elif rgb[0:1] == b"#":
|
||||
# FIXME: handle colour names (see ImagePalette.py)
|
||||
rgb = int(rgb[1:], 16)
|
||||
palette[c] = o8((rgb >> 16) & 255) +\
|
||||
o8((rgb >> 8) & 255) +\
|
||||
o8(rgb & 255)
|
||||
palette[c] = (o8((rgb >> 16) & 255) +
|
||||
o8((rgb >> 8) & 255) +
|
||||
o8(rgb & 255))
|
||||
else:
|
||||
# unknown colour
|
||||
raise ValueError("cannot read this XPM file")
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
# ;-)
|
||||
|
||||
VERSION = '1.1.7' # PIL version
|
||||
PILLOW_VERSION = '2.5.0' # Pillow
|
||||
PILLOW_VERSION = '2.5.3' # Pillow
|
||||
|
||||
_plugins = ['BmpImagePlugin',
|
||||
'BufrStubImagePlugin',
|
||||
|
@ -36,6 +36,7 @@ _plugins = ['BmpImagePlugin',
|
|||
'McIdasImagePlugin',
|
||||
'MicImagePlugin',
|
||||
'MpegImagePlugin',
|
||||
'MpoImagePlugin',
|
||||
'MspImagePlugin',
|
||||
'PalmImagePlugin',
|
||||
'PcdImagePlugin',
|
||||
|
|
|
@ -3,20 +3,25 @@ import os
|
|||
if bytes is str:
|
||||
def isStringType(t):
|
||||
return isinstance(t, basestring)
|
||||
|
||||
def isPath(f):
|
||||
return isinstance(f, basestring)
|
||||
else:
|
||||
def isStringType(t):
|
||||
return isinstance(t, str)
|
||||
|
||||
def isPath(f):
|
||||
return isinstance(f, (bytes, str))
|
||||
|
||||
|
||||
# Checks if an object is a string, and that it points to a directory.
|
||||
def isDirectory(f):
|
||||
return isPath(f) and os.path.isdir(f)
|
||||
|
||||
|
||||
class deferred_error(object):
|
||||
def __init__(self, ex):
|
||||
self.ex = ex
|
||||
|
||||
def __getattr__(self, elt):
|
||||
raise self.ex
|
||||
|
|
|
@ -3,7 +3,7 @@ Pillow
|
|||
|
||||
*Python Imaging Library (Fork)*
|
||||
|
||||
Pillow is the "friendly" PIL fork by Alex Clark and Contributors. PIL is the Python Imaging Library by Fredrik Lundh and Contributors. For more information, please `read the documentation <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
|
||||
:target: https://travis-ci.org/python-pillow/Pillow
|
||||
|
@ -20,3 +20,6 @@ Pillow is the "friendly" PIL fork by Alex Clark and Contributors. PIL is the Pyt
|
|||
.. image:: https://coveralls.io/repos/python-pillow/Pillow/badge.png?branch=master
|
||||
:target: https://coveralls.io/r/python-pillow/Pillow?branch=master
|
||||
|
||||
.. image:: https://landscape.io/github/python-pillow/Pillow/master/landscape.png
|
||||
:target: https://landscape.io/github/python-pillow/Pillow/master
|
||||
:alt: Code Health
|
||||
|
|
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``.
|
||||
|
||||
Depedencies
|
||||
-----------
|
||||
|
||||
Install::
|
||||
|
||||
pip install coverage nose
|
||||
|
||||
If you're using Python 2.6, there's one additional dependency::
|
||||
|
||||
pip install unittest2
|
||||
|
||||
Execution
|
||||
---------
|
||||
|
||||
Run the tests from the root of the Pillow source distribution::
|
||||
|
||||
python selftest.py
|
||||
nosetests Tests/test_*.py
|
||||
|
||||
Or with coverage::
|
||||
|
||||
coverage run --append --include=PIL/* selftest.py
|
||||
coverage run --append --include=PIL/* -m nose Tests/test_*.py
|
||||
coverage report
|
||||
coverage html
|
||||
open htmlcov/index.html
|
||||
**If Pillow has been built in-place**
|
||||
|
||||
To run an individual test::
|
||||
|
||||
python Tests/test_image.py
|
||||
|
||||
Run all the tests from the root of the Pillow source distribution::
|
||||
|
||||
nosetests -vx Tests/test_*.py
|
||||
|
||||
Or with coverage::
|
||||
|
||||
coverage run --append --include=PIL/* -m nose -vx Tests/test_*.py
|
||||
coverage report
|
||||
coverage html
|
||||
open htmlcov/index.html
|
||||
|
||||
**If Pillow has been installed**
|
||||
|
||||
To run an individual test::
|
||||
|
||||
./test-installed.py Tests/test_image.py
|
||||
|
||||
Run all the tests from the root of the Pillow source distribution::
|
||||
|
||||
./test-installed.py
|
||||
|
||||
|
||||
|
||||
|
|
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 tempfile
|
||||
import os
|
||||
import glob
|
||||
|
||||
if sys.version_info[:2] <= (2, 6):
|
||||
import unittest2 as unittest
|
||||
else:
|
||||
import unittest
|
||||
|
||||
def tearDownModule():
|
||||
#remove me later
|
||||
pass
|
||||
|
||||
class PillowTestCase(unittest.TestCase):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
unittest.TestCase.__init__(self, *args, **kwargs)
|
||||
self.currentResult = None # holds last result object passed to run method
|
||||
# holds last result object passed to run method:
|
||||
self.currentResult = None
|
||||
|
||||
# Nicer output for --verbose
|
||||
def __str__(self):
|
||||
return self.__class__.__name__ + "." + self._testMethodName
|
||||
|
||||
def run(self, result=None):
|
||||
self.currentResult = result # remember result for use later
|
||||
|
@ -40,7 +41,7 @@ class PillowTestCase(unittest.TestCase):
|
|||
except OSError:
|
||||
pass # report?
|
||||
else:
|
||||
print("=== orphaned temp file: %s" %path)
|
||||
print("=== orphaned temp file: %s" % path)
|
||||
|
||||
def assert_almost_equal(self, a, b, msg=None, eps=1e-6):
|
||||
self.assertLess(
|
||||
|
@ -99,7 +100,7 @@ class PillowTestCase(unittest.TestCase):
|
|||
ave_diff = float(diff)/(a.size[0]*a.size[1])
|
||||
self.assertGreaterEqual(
|
||||
epsilon, ave_diff,
|
||||
msg or "average pixel value difference %.4f > epsilon %.4f" % (
|
||||
(msg or '') + " average pixel value difference %.4f > epsilon %.4f" % (
|
||||
ave_diff, epsilon))
|
||||
|
||||
def assert_warning(self, warn_class, func):
|
||||
|
@ -123,7 +124,8 @@ class PillowTestCase(unittest.TestCase):
|
|||
self.assertTrue(found)
|
||||
return result
|
||||
|
||||
def skipKnownBadTest(self, msg=None, platform=None, travis=None):
|
||||
def skipKnownBadTest(self, msg=None, platform=None,
|
||||
travis=None, interpreter=None):
|
||||
# Skip if platform/travis matches, and
|
||||
# PILLOW_RUN_KNOWN_BAD is not true in the environment.
|
||||
if bool(os.environ.get('PILLOW_RUN_KNOWN_BAD', False)):
|
||||
|
@ -134,7 +136,9 @@ class PillowTestCase(unittest.TestCase):
|
|||
if platform is not None:
|
||||
skip = sys.platform.startswith(platform)
|
||||
if travis is not None:
|
||||
skip = skip and (travis == bool(os.environ.get('TRAVIS',False)))
|
||||
skip = skip and (travis == bool(os.environ.get('TRAVIS', False)))
|
||||
if interpreter is not None:
|
||||
skip = skip and (interpreter == 'pypy' and hasattr(sys, 'pypy_version_info'))
|
||||
if skip:
|
||||
self.skipTest(msg or "Known Bad Test")
|
||||
|
||||
|
@ -142,8 +146,8 @@ class PillowTestCase(unittest.TestCase):
|
|||
assert template[:5] in ("temp.", "temp_")
|
||||
(fd, path) = tempfile.mkstemp(template[4:], template[:4])
|
||||
os.close(fd)
|
||||
|
||||
self.addCleanup(self.delete_tempfile, path)
|
||||
|
||||
self.addCleanup(self.delete_tempfile, path)
|
||||
return path
|
||||
|
||||
def open_withImagemagick(self, f):
|
||||
|
@ -155,8 +159,8 @@ class PillowTestCase(unittest.TestCase):
|
|||
from PIL import Image
|
||||
return Image.open(outfile)
|
||||
raise IOError()
|
||||
|
||||
|
||||
|
||||
|
||||
# helpers
|
||||
|
||||
import sys
|
||||
|
@ -176,6 +180,26 @@ def tostring(im, format, **options):
|
|||
return out.getvalue()
|
||||
|
||||
|
||||
def hopper(mode="RGB", cache={}):
|
||||
from PIL import Image
|
||||
im = None
|
||||
# FIXME: Implement caching to reduce reading from disk but so an original
|
||||
# copy is returned each time and the cached image isn't modified by tests
|
||||
# (for fast, isolated, repeatable tests).
|
||||
# im = cache.get(mode)
|
||||
if im is None:
|
||||
if mode == "RGB":
|
||||
im = Image.open("Tests/images/hopper.ppm")
|
||||
elif mode == "F":
|
||||
im = lena("L").convert(mode)
|
||||
elif mode[:4] == "I;16":
|
||||
im = lena("I").convert(mode)
|
||||
else:
|
||||
im = lena("RGB").convert(mode)
|
||||
# cache[mode] = im
|
||||
return im
|
||||
|
||||
|
||||
def lena(mode="RGB", cache={}):
|
||||
from PIL import Image
|
||||
im = None
|
||||
|
@ -210,17 +234,21 @@ def command_succeeds(cmd):
|
|||
return False
|
||||
return True
|
||||
|
||||
|
||||
def djpeg_available():
|
||||
return command_succeeds(['djpeg', '--help'])
|
||||
|
||||
|
||||
def cjpeg_available():
|
||||
return command_succeeds(['cjpeg', '--help'])
|
||||
|
||||
|
||||
def netpbm_available():
|
||||
return command_succeeds(["ppmquant", "--help"]) and \
|
||||
command_succeeds(["ppmtogif", "--help"])
|
||||
return (command_succeeds(["ppmquant", "--help"]) and
|
||||
command_succeeds(["ppmtogif", "--help"]))
|
||||
|
||||
|
||||
def imagemagick_available():
|
||||
return command_succeeds(['convert', '-version'])
|
||||
|
||||
|
||||
# End of file
|
||||
|
|
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",
|
||||
"CMYK",
|
||||
"YCbCr",
|
||||
"LAB", "HSV",
|
||||
]
|
||||
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
from helper import unittest, PillowTestCase, tearDownModule
|
||||
from helper import unittest, PillowTestCase
|
||||
|
||||
import PIL
|
||||
import PIL.Image
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
from helper import unittest, PillowTestCase, tearDownModule
|
||||
from helper import unittest, PillowTestCase
|
||||
|
||||
from PIL import Image
|
||||
import os
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
from helper import unittest, PillowTestCase, tearDownModule, lena
|
||||
from helper import unittest, PillowTestCase, lena
|
||||
|
||||
try:
|
||||
import cffi
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
from helper import unittest, PillowTestCase, tearDownModule
|
||||
from helper import unittest, PillowTestCase
|
||||
|
||||
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
|
||||
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
|
||||
import io
|
||||
|
@ -63,6 +63,17 @@ class TestFileEps(PillowTestCase):
|
|||
with io.open(self.tempfile('temp_iobase.eps'), 'wb') as fh:
|
||||
image1.save(fh, 'EPS')
|
||||
|
||||
def test_bytesio_object(self):
|
||||
with open(file1, 'rb') as f:
|
||||
img_bytes = io.BytesIO(f.read())
|
||||
|
||||
img = Image.open(img_bytes)
|
||||
img.load()
|
||||
|
||||
image1_scale1_compare = Image.open(file1_compare).convert("RGB")
|
||||
image1_scale1_compare.load()
|
||||
self.assert_image_similar(img, image1_scale1_compare, 5)
|
||||
|
||||
def test_render_scale1(self):
|
||||
# We need png support for these render test
|
||||
codecs = dir(Image.core)
|
||||
|
@ -137,6 +148,83 @@ class TestFileEps(PillowTestCase):
|
|||
# open image with binary preview
|
||||
Image.open(file3)
|
||||
|
||||
def _test_readline(self,t, ending):
|
||||
ending = "Failure with line ending: %s" %("".join("%s" %ord(s) for s in ending))
|
||||
self.assertEqual(t.readline().strip('\r\n'), 'something', ending)
|
||||
self.assertEqual(t.readline().strip('\r\n'), 'else', ending)
|
||||
self.assertEqual(t.readline().strip('\r\n'), 'baz', ending)
|
||||
self.assertEqual(t.readline().strip('\r\n'), 'bif', ending)
|
||||
|
||||
def _test_readline_stringio(self, test_string, ending):
|
||||
# check all the freaking line endings possible
|
||||
try:
|
||||
import StringIO
|
||||
except:
|
||||
# don't skip, it skips everything in the parent test
|
||||
return
|
||||
t = StringIO.StringIO(test_string)
|
||||
self._test_readline(t, ending)
|
||||
|
||||
def _test_readline_io(self, test_string, ending):
|
||||
import io
|
||||
if str is bytes:
|
||||
t = io.StringIO(unicode(test_string))
|
||||
else:
|
||||
t = io.StringIO(test_string)
|
||||
self._test_readline(t, ending)
|
||||
|
||||
def _test_readline_file_universal(self, test_string, ending):
|
||||
f = self.tempfile('temp.txt')
|
||||
with open(f,'wb') as w:
|
||||
if str is bytes:
|
||||
w.write(test_string)
|
||||
else:
|
||||
w.write(test_string.encode('UTF-8'))
|
||||
|
||||
with open(f,'rU') as t:
|
||||
self._test_readline(t, ending)
|
||||
|
||||
def _test_readline_file_psfile(self, test_string, ending):
|
||||
f = self.tempfile('temp.txt')
|
||||
with open(f,'wb') as w:
|
||||
if str is bytes:
|
||||
w.write(test_string)
|
||||
else:
|
||||
w.write(test_string.encode('UTF-8'))
|
||||
|
||||
with open(f,'rb') as r:
|
||||
t = EpsImagePlugin.PSFile(r)
|
||||
self._test_readline(t, ending)
|
||||
|
||||
def test_readline(self):
|
||||
# check all the freaking line endings possible from the spec
|
||||
#test_string = u'something\r\nelse\n\rbaz\rbif\n'
|
||||
line_endings = ['\r\n', '\n']
|
||||
not_working_endings = ['\n\r', '\r']
|
||||
strings = ['something', 'else', 'baz', 'bif']
|
||||
|
||||
for ending in line_endings:
|
||||
s = ending.join(strings)
|
||||
# Native python versions will pass these endings.
|
||||
#self._test_readline_stringio(s, ending)
|
||||
#self._test_readline_io(s, ending)
|
||||
#self._test_readline_file_universal(s, ending)
|
||||
|
||||
self._test_readline_file_psfile(s, ending)
|
||||
|
||||
for ending in not_working_endings:
|
||||
# these only work with the PSFile, while they're in spec,
|
||||
# they're not likely to be used
|
||||
s = ending.join(strings)
|
||||
|
||||
# Native python versions may fail on these endings.
|
||||
#self._test_readline_stringio(s, ending)
|
||||
#self._test_readline_io(s, ending)
|
||||
#self._test_readline_file_universal(s, ending)
|
||||
|
||||
self._test_readline_file_psfile(s, ending)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
from helper import unittest, PillowTestCase, tearDownModule
|
||||
from helper import unittest, PillowTestCase
|
||||
|
||||
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 GifImagePlugin
|
||||
|
@ -6,8 +6,9 @@ from PIL import GifImagePlugin
|
|||
codecs = dir(Image.core)
|
||||
|
||||
# sample gif stream
|
||||
file = "Tests/images/lena.gif"
|
||||
with open(file, "rb") as f:
|
||||
TEST_GIF = "Tests/images/hopper.gif"
|
||||
|
||||
with open(TEST_GIF, "rb") as f:
|
||||
data = f.read()
|
||||
|
||||
|
||||
|
@ -18,7 +19,7 @@ class TestFileGif(PillowTestCase):
|
|||
self.skipTest("gif support not available") # can this happen?
|
||||
|
||||
def test_sanity(self):
|
||||
im = Image.open(file)
|
||||
im = Image.open(TEST_GIF)
|
||||
im.load()
|
||||
self.assertEqual(im.mode, "P")
|
||||
self.assertEqual(im.size, (128, 128))
|
||||
|
@ -35,9 +36,17 @@ class TestFileGif(PillowTestCase):
|
|||
self.assertEqual(test(0), 800)
|
||||
self.assertEqual(test(1), 38)
|
||||
|
||||
def test_optimize_full_l(self):
|
||||
from io import BytesIO
|
||||
|
||||
im = Image.frombytes("L", (16, 16), bytes(bytearray(range(256))))
|
||||
file = BytesIO()
|
||||
im.save(file, "GIF", optimize=True)
|
||||
self.assertEqual(im.mode, "L")
|
||||
|
||||
def test_roundtrip(self):
|
||||
out = self.tempfile('temp.gif')
|
||||
im = lena()
|
||||
im = hopper()
|
||||
im.save(out)
|
||||
reread = Image.open(out)
|
||||
|
||||
|
@ -46,17 +55,17 @@ class TestFileGif(PillowTestCase):
|
|||
def test_roundtrip2(self):
|
||||
# see https://github.com/python-pillow/Pillow/issues/403
|
||||
out = self.tempfile('temp.gif')
|
||||
im = Image.open('Tests/images/lena.gif')
|
||||
im = Image.open(TEST_GIF)
|
||||
im2 = im.copy()
|
||||
im2.save(out)
|
||||
reread = Image.open(out)
|
||||
|
||||
self.assert_image_similar(reread.convert('RGB'), lena(), 50)
|
||||
self.assert_image_similar(reread.convert('RGB'), hopper(), 50)
|
||||
|
||||
def test_palette_handling(self):
|
||||
# see https://github.com/python-pillow/Pillow/issues/513
|
||||
|
||||
im = Image.open('Tests/images/lena.gif')
|
||||
im = Image.open(TEST_GIF)
|
||||
im = im.convert('RGB')
|
||||
|
||||
im = im.resize((100, 100), Image.ANTIALIAS)
|
||||
|
@ -92,7 +101,7 @@ class TestFileGif(PillowTestCase):
|
|||
|
||||
@unittest.skipUnless(netpbm_available(), "netpbm not available")
|
||||
def test_save_netpbm_bmp_mode(self):
|
||||
img = Image.open(file).convert("RGB")
|
||||
img = Image.open(TEST_GIF).convert("RGB")
|
||||
|
||||
tempfile = self.tempfile("temp.gif")
|
||||
GifImagePlugin._save_netpbm(img, 0, tempfile)
|
||||
|
@ -100,12 +109,56 @@ class TestFileGif(PillowTestCase):
|
|||
|
||||
@unittest.skipUnless(netpbm_available(), "netpbm not available")
|
||||
def test_save_netpbm_l_mode(self):
|
||||
img = Image.open(file).convert("L")
|
||||
img = Image.open(TEST_GIF).convert("L")
|
||||
|
||||
tempfile = self.tempfile("temp.gif")
|
||||
GifImagePlugin._save_netpbm(img, 0, tempfile)
|
||||
self.assert_image_similar(img, Image.open(tempfile).convert("L"), 0)
|
||||
|
||||
def test_seek(self):
|
||||
img = Image.open("Tests/images/dispose_none.gif")
|
||||
framecount = 0
|
||||
try:
|
||||
while True:
|
||||
framecount += 1
|
||||
img.seek(img.tell() +1)
|
||||
except EOFError:
|
||||
self.assertEqual(framecount, 5)
|
||||
|
||||
def test_dispose_none(self):
|
||||
img = Image.open("Tests/images/dispose_none.gif")
|
||||
try:
|
||||
while True:
|
||||
img.seek(img.tell() +1)
|
||||
self.assertEqual(img.disposal_method, 1)
|
||||
except EOFError:
|
||||
pass
|
||||
|
||||
def test_dispose_background(self):
|
||||
img = Image.open("Tests/images/dispose_bgnd.gif")
|
||||
try:
|
||||
while True:
|
||||
img.seek(img.tell() +1)
|
||||
self.assertEqual(img.disposal_method, 2)
|
||||
except EOFError:
|
||||
pass
|
||||
|
||||
def test_dispose_previous(self):
|
||||
img = Image.open("Tests/images/dispose_prev.gif")
|
||||
try:
|
||||
while True:
|
||||
img.seek(img.tell() +1)
|
||||
self.assertEqual(img.disposal_method, 3)
|
||||
except EOFError:
|
||||
pass
|
||||
|
||||
def test_iss634(self):
|
||||
img = Image.open("Tests/images/iss634.gif")
|
||||
# seek to the second frame
|
||||
img.seek(img.tell() +1)
|
||||
# all transparent pixels should be replaced with the color from the first frame
|
||||
self.assertEqual(img.histogram()[img.info['transparency']], 0)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
|
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
|
||||
|
||||
|
|
|
@ -1,16 +1,16 @@
|
|||
from helper import unittest, PillowTestCase, tearDownModule
|
||||
from helper import unittest, PillowTestCase
|
||||
|
||||
from PIL import Image
|
||||
|
||||
# sample ppm stream
|
||||
file = "Tests/images/lena.ico"
|
||||
data = open(file, "rb").read()
|
||||
TEST_ICO_FILE = "Tests/images/hopper.ico"
|
||||
TEST_DATA = open(TEST_ICO_FILE, "rb").read()
|
||||
|
||||
|
||||
class TestFileIco(PillowTestCase):
|
||||
|
||||
def test_sanity(self):
|
||||
im = Image.open(file)
|
||||
im = Image.open(TEST_ICO_FILE)
|
||||
im.load()
|
||||
self.assertEqual(im.mode, "RGBA")
|
||||
self.assertEqual(im.size, (16, 16))
|
||||
|
|
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
|
||||
|
||||
import random
|
||||
|
@ -60,7 +60,8 @@ class TestFileJpeg(PillowTestCase):
|
|||
self.assertGreater(y, 0.8)
|
||||
self.assertEqual(k, 0.0)
|
||||
# the opposite corner is black
|
||||
c, m, y, k = [x / 255.0 for x in im.getpixel((im.size[0]-1, im.size[1]-1))]
|
||||
c, m, y, k = [x / 255.0 for x in im.getpixel((
|
||||
im.size[0]-1, im.size[1]-1))]
|
||||
self.assertGreater(k, 0.9)
|
||||
# roundtrip, and check again
|
||||
im = self.roundtrip(im)
|
||||
|
@ -69,7 +70,8 @@ class TestFileJpeg(PillowTestCase):
|
|||
self.assertGreater(m, 0.8)
|
||||
self.assertGreater(y, 0.8)
|
||||
self.assertEqual(k, 0.0)
|
||||
c, m, y, k = [x / 255.0 for x in im.getpixel((im.size[0]-1, im.size[1]-1))]
|
||||
c, m, y, k = [x / 255.0 for x in im.getpixel((
|
||||
im.size[0]-1, im.size[1]-1))]
|
||||
self.assertGreater(k, 0.9)
|
||||
|
||||
def test_dpi(self):
|
||||
|
@ -150,7 +152,8 @@ class TestFileJpeg(PillowTestCase):
|
|||
if py3:
|
||||
a = bytes(random.randint(0, 255) for _ in range(256 * 256 * 3))
|
||||
else:
|
||||
a = b''.join(chr(random.randint(0, 255)) for _ in range(256 * 256 * 3))
|
||||
a = b''.join(chr(random.randint(0, 255)) for _ in range(
|
||||
256 * 256 * 3))
|
||||
im = Image.frombuffer("RGB", (256, 256), a, "raw", "RGB", 0, 1)
|
||||
# this requires more bytes than pixels in the image
|
||||
im.save(f, format="JPEG", progressive=True, quality=100)
|
||||
|
@ -216,10 +219,23 @@ class TestFileJpeg(PillowTestCase):
|
|||
info = im._getexif()
|
||||
self.assertEqual(info[305], 'Adobe Photoshop CS Macintosh')
|
||||
|
||||
def test_mp(self):
|
||||
im = Image.open("Tests/images/pil_sample_rgb.jpg")
|
||||
self.assertIsNone(im._getmp())
|
||||
|
||||
def test_quality_keep(self):
|
||||
# RGB
|
||||
im = Image.open("Tests/images/lena.jpg")
|
||||
f = self.tempfile('temp.jpg')
|
||||
im.save(f, quality='keep')
|
||||
# Grayscale
|
||||
im = Image.open("Tests/images/lena_gray.jpg")
|
||||
f = self.tempfile('temp.jpg')
|
||||
im.save(f, quality='keep')
|
||||
# CMYK
|
||||
im = Image.open("Tests/images/pil_sample_cmyk.jpg")
|
||||
f = self.tempfile('temp.jpg')
|
||||
im.save(f, quality='keep')
|
||||
|
||||
def test_junk_jpeg_header(self):
|
||||
# https://github.com/python-pillow/Pillow/issues/630
|
||||
|
@ -231,11 +247,13 @@ class TestFileJpeg(PillowTestCase):
|
|||
qtables = im.quantization
|
||||
reloaded = self.roundtrip(im, qtables=qtables, subsampling=0)
|
||||
self.assertEqual(im.quantization, reloaded.quantization)
|
||||
self.assert_image_similar(im, self.roundtrip(im, qtables='web_low'), 30)
|
||||
self.assert_image_similar(im, self.roundtrip(im, qtables='web_high'), 30)
|
||||
self.assert_image_similar(im, self.roundtrip(im, qtables='web_low'),
|
||||
30)
|
||||
self.assert_image_similar(im, self.roundtrip(im, qtables='web_high'),
|
||||
30)
|
||||
self.assert_image_similar(im, self.roundtrip(im, qtables='keep'), 30)
|
||||
|
||||
#values from wizard.txt in jpeg9-a src package.
|
||||
# values from wizard.txt in jpeg9-a src package.
|
||||
standard_l_qtable = [int(s) for s in """
|
||||
16 11 10 16 24 40 51 61
|
||||
12 12 14 19 26 58 60 55
|
||||
|
@ -247,7 +265,7 @@ class TestFileJpeg(PillowTestCase):
|
|||
72 92 95 98 112 100 103 99
|
||||
""".split(None)]
|
||||
|
||||
standard_chrominance_qtable= [int(s) for s in """
|
||||
standard_chrominance_qtable = [int(s) for s in """
|
||||
17 18 24 47 99 99 99 99
|
||||
18 21 26 66 99 99 99 99
|
||||
24 26 56 99 99 99 99 99
|
||||
|
@ -272,8 +290,8 @@ class TestFileJpeg(PillowTestCase):
|
|||
# dict of qtable lists
|
||||
self.assert_image_similar(im,
|
||||
self.roundtrip(im,
|
||||
qtables={0:standard_l_qtable,
|
||||
1:standard_chrominance_qtable}),
|
||||
qtables={0: standard_l_qtable,
|
||||
1: standard_chrominance_qtable}),
|
||||
30)
|
||||
|
||||
@unittest.skipUnless(djpeg_available(), "djpeg not available")
|
||||
|
@ -291,6 +309,15 @@ class TestFileJpeg(PillowTestCase):
|
|||
# Default save quality is 75%, so a tiny bit of difference is alright
|
||||
self.assert_image_similar(img, Image.open(tempfile), 1)
|
||||
|
||||
def test_no_duplicate_0x1001_tag(self):
|
||||
# Arrange
|
||||
from PIL import ExifTags
|
||||
tag_ids = dict(zip(ExifTags.TAGS.values(), ExifTags.TAGS.keys()))
|
||||
|
||||
# Assert
|
||||
self.assertEqual(tag_ids['RelatedImageWidth'], 0x1001)
|
||||
self.assertEqual(tag_ids['RelatedImageLength'], 0x1002)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
from helper import unittest, PillowTestCase, tearDownModule
|
||||
from helper import unittest, PillowTestCase
|
||||
|
||||
from PIL import Image
|
||||
from io import BytesIO
|
||||
|
@ -52,7 +52,8 @@ class TestFileJpeg2k(PillowTestCase):
|
|||
def test_lossless(self):
|
||||
im = Image.open('Tests/images/test-card-lossless.jp2')
|
||||
im.load()
|
||||
im.save('/tmp/test-card.png')
|
||||
outfile = self.tempfile('temp_test-card.png')
|
||||
im.save(outfile)
|
||||
self.assert_image_similar(im, test_card, 1.0e-3)
|
||||
|
||||
def test_lossy_tiled(self):
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
from helper import unittest, PillowTestCase, tearDownModule, lena, py3
|
||||
from helper import unittest, PillowTestCase, lena, py3
|
||||
|
||||
import os
|
||||
|
||||
from PIL import Image, TiffImagePlugin
|
||||
|
||||
|
||||
class TestFileLibTiff(PillowTestCase):
|
||||
class LibTiffTestCase(PillowTestCase):
|
||||
|
||||
def setUp(self):
|
||||
codecs = dir(Image.core)
|
||||
|
@ -32,6 +31,8 @@ class TestFileLibTiff(PillowTestCase):
|
|||
out = self.tempfile("temp.png")
|
||||
im.save(out)
|
||||
|
||||
class TestFileLibTiff(LibTiffTestCase):
|
||||
|
||||
def test_g4_tiff(self):
|
||||
"""Test the ordinary file path load path"""
|
||||
|
||||
|
@ -311,6 +312,35 @@ class TestFileLibTiff(PillowTestCase):
|
|||
self.assertRaises(OSError, lambda: os.fstat(fn))
|
||||
self.assertRaises(OSError, lambda: os.close(fn))
|
||||
|
||||
def test_multipage(self):
|
||||
# issue #862
|
||||
TiffImagePlugin.READ_LIBTIFF = True
|
||||
im = Image.open('Tests/images/multipage.tiff')
|
||||
# file is a multipage tiff, 10x10 green, 10x10 red, 20x20 blue
|
||||
|
||||
im.seek(0)
|
||||
self.assertEqual(im.size, (10,10))
|
||||
self.assertEqual(im.convert('RGB').getpixel((0,0)), (0,128,0))
|
||||
self.assertTrue(im.tag.next)
|
||||
|
||||
im.seek(1)
|
||||
self.assertEqual(im.size, (10,10))
|
||||
self.assertEqual(im.convert('RGB').getpixel((0,0)), (255,0,0))
|
||||
self.assertTrue(im.tag.next)
|
||||
|
||||
im.seek(2)
|
||||
self.assertFalse(im.tag.next)
|
||||
self.assertEqual(im.size, (20,20))
|
||||
self.assertEqual(im.convert('RGB').getpixel((0,0)), (0,0,255))
|
||||
|
||||
TiffImagePlugin.READ_LIBTIFF = False
|
||||
|
||||
def test__next(self):
|
||||
TiffImagePlugin.READ_LIBTIFF = True
|
||||
im = Image.open('Tests/images/lena.tif')
|
||||
self.assertFalse(im.tag.next)
|
||||
im.load()
|
||||
self.assertFalse(im.tag.next)
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
|