Merge with master

This commit is contained in:
Hugo 2014-09-18 11:12:50 +03:00
commit c1dadc3fa6
227 changed files with 4444 additions and 1410 deletions

View File

@ -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
View File

@ -60,3 +60,9 @@ docs/_build/
\#*#
.#*
#Komodo
*.komodoproject
#OS
.DS_Store

View File

@ -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

View File

@ -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
View 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?

View File

@ -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

View File

@ -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&

View File

@ -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

View File

@ -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.

View File

@ -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:

View File

@ -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",

View File

@ -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

View 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

View File

@ -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")

View File

@ -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)

View File

@ -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

View 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

View File

@ -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

View File

@ -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

View 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))

View File

@ -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):

View File

@ -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.
"""

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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
View 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")

View File

@ -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:

View File

@ -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 ***")

View File

@ -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

View File

@ -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,

View File

@ -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

View 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

View File

@ -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")

View File

@ -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
#

View File

@ -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
}
##

View File

@ -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")

View 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',

View File

@ -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

View File

@ -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

View 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

View 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)

View File

@ -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
View 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
View 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
View 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()

View File

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 586 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

BIN
Tests/images/frozenpond.mpo Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 162 KiB

View 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

View 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

Binary file not shown.

BIN
Tests/images/hopper.dcx Normal file

Binary file not shown.

BIN
Tests/images/hopper.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
Tests/images/hopper.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
Tests/images/hopper.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

BIN
Tests/images/hopper.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

BIN
Tests/images/hopper.ppm Normal file

Binary file not shown.

BIN
Tests/images/hopper.ras Normal file

Binary file not shown.

BIN
Tests/images/hopper.rgb Normal file

Binary file not shown.

BIN
Tests/images/hopper.spider Normal file

Binary file not shown.

BIN
Tests/images/hopper.tar Normal file

Binary file not shown.

BIN
Tests/images/iptc.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

BIN
Tests/images/iss634.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 271 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Binary file not shown.

BIN
Tests/images/lena_gray.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

BIN
Tests/images/multipage.tiff Normal file

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

BIN
Tests/images/sugarshack.mpo Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

Binary file not shown.

View File

@ -9,6 +9,7 @@ modes = [
"RGB", "RGBA", "RGBa", "RGBX",
"CMYK",
"YCbCr",
"LAB", "HSV",
]

View File

@ -1,4 +1,4 @@
from helper import unittest, PillowTestCase, tearDownModule
from helper import unittest, PillowTestCase
import PIL
import PIL.Image

View File

@ -1,4 +1,4 @@
from helper import unittest, PillowTestCase, tearDownModule
from helper import unittest, PillowTestCase
from PIL import Image
import os

View File

@ -1,4 +1,4 @@
from helper import unittest, PillowTestCase, tearDownModule, lena
from helper import unittest, PillowTestCase, lena
try:
import cffi

View File

@ -1,4 +1,4 @@
from helper import unittest, PillowTestCase, tearDownModule
from helper import unittest, PillowTestCase
from PIL import Image

View File

@ -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
View 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
View 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

View 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()

View File

@ -1,4 +1,4 @@
from helper import unittest, PillowTestCase, tearDownModule
from helper import unittest, PillowTestCase
from PIL import Image

View File

@ -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()

View 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

View File

@ -1,4 +1,4 @@
from helper import unittest, PillowTestCase, tearDownModule
from helper import unittest, PillowTestCase
from PIL import Image

View File

@ -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
View 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

View 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()

View File

@ -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):

View File

@ -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()

Some files were not shown because too many files have changed in this diff Show More