Merge from master
|
@ -9,3 +9,6 @@ exclude_lines =
|
|||
# Don't complain if non-runnable code isn't run:
|
||||
if 0:
|
||||
if __name__ == .__main__.:
|
||||
# Don't complain about debug code
|
||||
if Image.DEBUG:
|
||||
if DEBUG:
|
||||
|
|
6
.gitignore
vendored
|
@ -60,3 +60,9 @@ docs/_build/
|
|||
\#*#
|
||||
.#*
|
||||
|
||||
#Komodo
|
||||
*.komodoproject
|
||||
|
||||
#OS
|
||||
.DS_Store
|
||||
|
||||
|
|
47
.travis.yml
|
@ -3,18 +3,25 @@ language: python
|
|||
notifications:
|
||||
irc: "chat.freenode.net#pil"
|
||||
|
||||
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 cmake"
|
||||
- "sudo apt-get -qq install libfreetype6-dev liblcms2-dev python-qt4 ghostscript libffi-dev libjpeg-turbo-progs cmake imagemagick lcov"
|
||||
- "pip install cffi"
|
||||
- "pip install coveralls"
|
||||
- "pip install coveralls nose coveralls-merge"
|
||||
- "gem install coveralls-lcov"
|
||||
- travis_retry pip install pyroma
|
||||
- if [ "$TRAVIS_PYTHON_VERSION" == "2.6" ]; then pip install unittest2; fi
|
||||
|
||||
# webp
|
||||
- pushd depends && ./install_webp.sh && popd
|
||||
|
@ -25,21 +32,31 @@ 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 python selftest.py; fi
|
||||
- if [ "$TRAVIS_PYTHON_VERSION" == "pypy" ]; then python Tests/run.py; fi
|
||||
|
||||
# Cover the others
|
||||
- if [ "$TRAVIS_PYTHON_VERSION" != "pypy" ]; then coverage run --append --include=PIL/* selftest.py; fi
|
||||
- if [ "$TRAVIS_PYTHON_VERSION" != "pypy" ]; then python Tests/run.py --coverage; fi
|
||||
- coverage run --append --include=PIL/* selftest.py
|
||||
- coverage run --append --include=PIL/* -m nose -vx Tests/test_*.py
|
||||
|
||||
after_success:
|
||||
# gather the coverage data
|
||||
- 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
|
||||
- coveralls-lcov -v -n coverage.filtered.info > coverage.c.json
|
||||
|
||||
- coverage report
|
||||
- coveralls
|
||||
- coveralls-merge coverage.c.json
|
||||
|
||||
|
||||
- pip install pep8 pyflakes
|
||||
- pep8 PIL/*.py
|
||||
- pyflakes PIL/*.py
|
||||
- pep8 Tests/*.py
|
||||
- pyflakes Tests/*.py
|
||||
- pep8 --statistics --count PIL/*.py
|
||||
- pep8 --statistics --count Tests/*.py
|
||||
- pyflakes PIL/*.py | tee >(wc -l)
|
||||
- pyflakes Tests/*.py | tee >(wc -l)
|
||||
|
||||
|
||||
# Coverage and quality reports on just the latest diff.
|
||||
# (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
|
||||
|
|
235
CHANGES.rst
|
@ -1,38 +1,194 @@
|
|||
Changelog (Pillow)
|
||||
==================
|
||||
|
||||
2.5.0 (unreleased)
|
||||
2.6.0 (unreleased)
|
||||
------------------
|
||||
|
||||
- Fixed CVE-2014-3598, a DOS in the Jpeg2KImagePlugin (backport)
|
||||
[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, 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)
|
||||
------------------
|
||||
|
||||
- Imagedraw rewrite
|
||||
[terseus, wiredfool]
|
||||
|
||||
- Add support for multithreaded test execution
|
||||
[wiredfool]
|
||||
|
||||
- Prevent shell injection #748
|
||||
[mbrown1413, wiredfool]
|
||||
|
||||
- Support for Resolution in BMP files #734
|
||||
[gcq]
|
||||
|
||||
- Fix error in setup.py for Python 3
|
||||
[matthew-brett]
|
||||
|
||||
- Pyroma fix and add Python 3.4 to setup metadata #742
|
||||
[wirefool]
|
||||
|
||||
- Top level flake8 fixes #741
|
||||
[aclark]
|
||||
|
||||
- Remove obsolete Animated Raster Graphics (ARG) support
|
||||
[hugovk]
|
||||
|
||||
- Fix test_imagedraw failures #727
|
||||
[cgohlke]
|
||||
|
||||
- Fix AttributeError: class Image has no attribute 'DEBUG' #726
|
||||
[cgohlke]
|
||||
|
||||
- Fix msvc warning: 'inline' : macro redefinition #725
|
||||
[cgohlke]
|
||||
|
||||
- Cleanup #654
|
||||
[dvska, hugovk, wiredfool]
|
||||
|
||||
- 16-bit monochrome support for JPEG2000
|
||||
[videan42]
|
||||
|
||||
- Fixed ImagePalette.save
|
||||
[brightpisces]
|
||||
|
||||
- Support JPEG qtables
|
||||
[csinchok]
|
||||
|
||||
- Add binary morphology addon
|
||||
[dov, wiredfool]
|
||||
|
||||
- Decompression bomb protection
|
||||
[hugovk]
|
||||
|
||||
- Put images in a single directory
|
||||
[hugovk]
|
||||
|
||||
- Support OpenJpeg 2.1
|
||||
[al45tair]
|
||||
|
||||
- Remove unistd.h #include for all platforms
|
||||
[wiredfool]
|
||||
|
||||
- Use unittest for tests
|
||||
[hugovk]
|
||||
|
||||
- ImageCms fixes
|
||||
[hugovk]
|
||||
|
||||
- Added more ImageDraw tests
|
||||
[hugovk]
|
||||
|
||||
- Added tests for Spider files
|
||||
[hugovk]
|
||||
|
||||
|
||||
- Use libtiff to write any compressed tiff files
|
||||
[wiredfool]
|
||||
|
||||
|
||||
- Support for pickling Image objects
|
||||
[hugovk]
|
||||
|
||||
- Fixed resolution handling for EPS thumbnails
|
||||
[eliempje]
|
||||
|
||||
|
||||
- Fixed rendering of some binary EPS files (Issue #302)
|
||||
[eliempje]
|
||||
|
||||
[eliempje]
|
||||
|
||||
- Rename variables not to use built-in function names
|
||||
[hugovk]
|
||||
|
||||
- Ignore junk JPEG markers
|
||||
|
||||
- Ignore junk JPEG markers
|
||||
[hugovk]
|
||||
|
||||
|
||||
- Change default interpolation for Image.thumbnail to Image.ANTIALIAS
|
||||
[hugovk]
|
||||
|
||||
|
||||
- Add tests and fixes for saving PDFs
|
||||
[hugovk]
|
||||
|
||||
|
||||
- Remove transparency resource after P->RGBA conversion
|
||||
[hugovk]
|
||||
|
||||
|
||||
- Clean up preprocessor cruft for Windows
|
||||
[CounterPillow]
|
||||
|
||||
|
@ -42,13 +198,13 @@ Changelog (Pillow)
|
|||
- Added Image.close, context manager support.
|
||||
[wiredfool]
|
||||
|
||||
- Added support for 16 bit PGM files.
|
||||
- Added support for 16 bit PGM files.
|
||||
[wiredfool]
|
||||
|
||||
- Updated OleFileIO to version 0.30 from upstream
|
||||
[hugovk]
|
||||
|
||||
- Added support for additional TIFF floating point format
|
||||
- Added support for additional TIFF floating point format
|
||||
[Hijackal]
|
||||
|
||||
- Have the tempfile use a suffix with a dot
|
||||
|
@ -78,7 +234,7 @@ Changelog (Pillow)
|
|||
- Added support for JPEG 2000
|
||||
[al45tair]
|
||||
|
||||
- Add more detailed error messages to Image.py
|
||||
- Add more detailed error messages to Image.py
|
||||
[larsmans]
|
||||
|
||||
- Avoid conflicting _expand functions in PIL & MINGW, fixes #538
|
||||
|
@ -106,7 +262,7 @@ Changelog (Pillow)
|
|||
[wiredfool]
|
||||
|
||||
- Fixed palette handling when converting from mode P->RGB->P
|
||||
[d_schmidt]
|
||||
[d_schmidt]
|
||||
|
||||
- Fixed saving mode P image as a PNG with transparency = palette color 0
|
||||
[d-schmidt]
|
||||
|
@ -116,7 +272,7 @@ Changelog (Pillow)
|
|||
|
||||
- Fixed DOS with invalid palette size or invalid image size in BMP file
|
||||
[wiredfool]
|
||||
|
||||
|
||||
- Added support for BMP version 4 and 5
|
||||
[eddwardo, wiredfool]
|
||||
|
||||
|
@ -149,7 +305,13 @@ 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)
|
||||
------------------
|
||||
|
||||
|
@ -274,7 +436,7 @@ Changelog (Pillow)
|
|||
[nikmolnar]
|
||||
|
||||
- Fix for encoding of b_whitespace, similar to closed issue #272
|
||||
[mhogg]
|
||||
[mhogg]
|
||||
|
||||
- Fix #273: Add numpy array interface support for 16 and 32 bit integer modes
|
||||
[cgohlke]
|
||||
|
@ -431,10 +593,14 @@ Changelog (Pillow)
|
|||
2.0.0 (2013-03-15)
|
||||
------------------
|
||||
|
||||
.. Note:: Special thanks to Christoph Gohlke and Eric Soroos for assisting with a pre-PyCon 2013 release!
|
||||
|
||||
- Many other bug fixes and enhancements by many other people.
|
||||
|
||||
- Add Python 3 support. (Pillow >= 2.0.0 supports Python 2.6, 2.7, 3.2, 3.3. Pillow < 2.0.0 supports Python 2.4, 2.5, 2.6, 2.7.)
|
||||
[fluggo]
|
||||
|
||||
- Add PyPy support (experimental, please see: https://github.com/python-imaging/Pillow/issues/67)
|
||||
- Add PyPy support (experimental, please see: https://github.com/python-pillow/Pillow/issues/67)
|
||||
|
||||
- Add WebP support.
|
||||
[lqs]
|
||||
|
@ -454,10 +620,6 @@ Changelog (Pillow)
|
|||
- Added support for PNG images with transparency palette.
|
||||
[d-schmidt]
|
||||
|
||||
- Many other bug fixes and enhancements by many other people (see commit log and/or docs/CONTRIBUTORS.txt).
|
||||
|
||||
- Special thanks to Christoph Gohlke and Eric Soroos for rallying around the effort to get a release out for PyCon 2013.
|
||||
|
||||
1.7.8 (2012-11-01)
|
||||
------------------
|
||||
|
||||
|
@ -530,44 +692,55 @@ Changelog (Pillow)
|
|||
[elro]
|
||||
|
||||
- Doc fixes
|
||||
[aclark]
|
||||
|
||||
1.5 (11/28/2010)
|
||||
----------------
|
||||
|
||||
- Module and package fixes
|
||||
[aclark]
|
||||
|
||||
1.4 (11/28/2010)
|
||||
----------------
|
||||
|
||||
- Doc fixes
|
||||
[aclark]
|
||||
|
||||
1.3 (11/28/2010)
|
||||
----------------
|
||||
|
||||
- Add support for /lib64 and /usr/lib64 library directories on Linux
|
||||
[aclark]
|
||||
|
||||
- Doc fixes
|
||||
[aclark]
|
||||
|
||||
1.2 (08/02/2010)
|
||||
----------------
|
||||
|
||||
- On OS X also check for freetype2 in the X11 path [jezdez]
|
||||
- Doc fixes [aclark]
|
||||
- On OS X also check for freetype2 in the X11 path
|
||||
[jezdez]
|
||||
|
||||
- Doc fixes
|
||||
[aclark]
|
||||
|
||||
1.1 (07/31/2010)
|
||||
----------------
|
||||
|
||||
- Removed setuptools_hg requirement
|
||||
[aclark]
|
||||
|
||||
- Doc fixes
|
||||
[aclark]
|
||||
|
||||
1.0 (07/30/2010)
|
||||
----------------
|
||||
|
||||
- Forked PIL based on Hanno Schlichting's re-packaging
|
||||
(http://dist.plone.org/thirdparty/PIL-1.1.7.tar.gz)
|
||||
- Remove support for ``import Image``, etc. from the standard namespace. ``from PIL import Image`` etc. now required.
|
||||
- Forked PIL based on `Hanno Schlichting's re-packaging <http://dist.plone.org/thirdparty/PIL-1.1.7.tar.gz>`_
|
||||
[aclark]
|
||||
|
||||
- Remove support for importing from the standard namespace
|
||||
|
||||
.. Note:: What follows is the original PIL 1.1.7 CHANGES file contents
|
||||
.. Note:: What follows is the original PIL 1.1.7 CHANGES
|
||||
|
||||
::
|
||||
|
||||
|
|
26
CONTRIBUTING.md
Normal file
|
@ -0,0 +1,26 @@
|
|||
# Contributing
|
||||
|
||||
## Fixes, Features and Changes
|
||||
|
||||
Send a pull request. We'll generally want documentation and [tests](Tests/README.rst) for new features. Tests or documentation on their own are also welcomed. Feel free to ask questions as an [issue](https://github.com/python-pillow/Pillow/issues/new) or on IRC (irc.freenode.net, #pil)
|
||||
|
||||
- Fork the repo
|
||||
- Make a branch
|
||||
- Add your changes + Tests
|
||||
- Run the test suite. Try to run on both Python 2.x and 3.x, or you'll get tripped up. You can enable [Travis CI on your repo](https://travis-ci.org/profile/) to catch test failures prior to the pull request.
|
||||
- Push to your fork, and make a pull request.
|
||||
|
||||
A few guidelines:
|
||||
- Try to keep any code commits clean and separate from reformatting commits.
|
||||
- All new code is going to need tests.
|
||||
- Try to follow PEP8.
|
||||
|
||||
## Bugs
|
||||
|
||||
When reporting bugs, please include example code that reproduces the issue, and if possible a problem image. The best reproductions are self-contained scripts that pull in as few dependencies as possible. An entire Django stack is harder to handle.
|
||||
|
||||
Let us know:
|
||||
- What did you do?
|
||||
- What did you expect to happen?
|
||||
- What actually happened?
|
||||
- What versions of Pillow and Python are you using?
|
34
MANIFEST.in
|
@ -1,7 +1,11 @@
|
|||
|
||||
include *.c
|
||||
include *.h
|
||||
include *.md
|
||||
include *.py
|
||||
include *.rst
|
||||
include *.txt
|
||||
include .coveragerc
|
||||
include .gitattributes
|
||||
include .travis.yml
|
||||
include Makefile
|
||||
|
@ -23,33 +27,59 @@ 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
|
||||
recursive-include Tests *.gif
|
||||
recursive-include Tests *.gnuplot
|
||||
recursive-include Tests *.html
|
||||
recursive-include Tests *.icm
|
||||
recursive-include Tests *.icns
|
||||
recursive-include Tests *.ico
|
||||
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
|
||||
recursive-include Tests *.pgm
|
||||
recursive-include Tests *.pil
|
||||
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
|
||||
recursive-include Tests *.tiff
|
||||
recursive-include Tests *.ttf
|
||||
recursive-include Tests *.txt
|
||||
recursive-include Tests *.webp
|
||||
recursive-include Tests *.xpm
|
||||
recursive-include Tk *.c
|
||||
recursive-include Tk *.rst
|
||||
recursive-include Tk *.txt
|
||||
recursive-include depends *.rst
|
||||
recursive-include depends *.sh
|
||||
recursive-include docs *.bat
|
||||
recursive-include docs *.gitignore
|
||||
|
@ -57,10 +87,10 @@ 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
|
||||
|
|
56
Makefile
|
@ -1,7 +1,61 @@
|
|||
.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 .
|
||||
bin/pip install -r requirements.txt
|
||||
bin/python setup.py develop
|
||||
bin/python selftest.py
|
||||
bin/python Tests/run.py
|
||||
bin/nosetests Tests/test_*.py
|
||||
bin/python setup.py install
|
||||
bin/python test-installed.py
|
||||
check-manifest
|
||||
pyroma .
|
||||
viewdoc
|
||||
|
||||
clean:
|
||||
python setup.py clean
|
||||
rm PIL/*.so || true
|
||||
rm -r build || true
|
||||
find . -name __pycache__ | xargs rm -r || true
|
||||
|
||||
install:
|
||||
python setup.py install
|
||||
python selftest.py --installed
|
||||
|
||||
test:
|
||||
python test-installed.py
|
||||
|
||||
inplace: clean
|
||||
python setup.py build_ext --inplace
|
||||
|
||||
coverage:
|
||||
# requires nose-cov
|
||||
coverage erase
|
||||
coverage run --parallel-mode --include=PIL/* selftest.py
|
||||
nosetests --with-cov --cov='PIL/' --cov-report=html Tests/test_*.py
|
||||
# doesn't combine properly before report,
|
||||
# writing report instead of displaying invalid report
|
||||
rm -r htmlcov || true
|
||||
coverage combine
|
||||
coverage report
|
||||
|
||||
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&
|
|
@ -1,506 +0,0 @@
|
|||
#
|
||||
# THIS IS WORK IN PROGRESS
|
||||
#
|
||||
# The Python Imaging Library.
|
||||
# $Id$
|
||||
#
|
||||
# ARG animation support code
|
||||
#
|
||||
# history:
|
||||
# 1996-12-30 fl Created
|
||||
# 1996-01-06 fl Added safe scripting environment
|
||||
# 1996-01-10 fl Added JHDR, UHDR and sYNC support
|
||||
# 2005-03-02 fl Removed AAPP and ARUN support
|
||||
#
|
||||
# Copyright (c) Secret Labs AB 1997.
|
||||
# Copyright (c) Fredrik Lundh 1996-97.
|
||||
#
|
||||
# See the README file for information on usage and redistribution.
|
||||
#
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
__version__ = "0.4"
|
||||
|
||||
from PIL import Image, ImageFile, ImagePalette
|
||||
|
||||
from PIL.PngImagePlugin import i8, i16, i32, ChunkStream, _MODES
|
||||
|
||||
MAGIC = b"\212ARG\r\n\032\n"
|
||||
|
||||
# --------------------------------------------------------------------
|
||||
# ARG parser
|
||||
|
||||
class ArgStream(ChunkStream):
|
||||
"Parser callbacks for ARG data"
|
||||
|
||||
def __init__(self, fp):
|
||||
|
||||
ChunkStream.__init__(self, fp)
|
||||
|
||||
self.eof = 0
|
||||
|
||||
self.im = None
|
||||
self.palette = None
|
||||
|
||||
self.__reset()
|
||||
|
||||
def __reset(self):
|
||||
|
||||
# reset decoder state (called on init and sync)
|
||||
|
||||
self.count = 0
|
||||
self.id = None
|
||||
self.action = ("NONE",)
|
||||
|
||||
self.images = {}
|
||||
self.names = {}
|
||||
|
||||
|
||||
def chunk_AHDR(self, offset, bytes):
|
||||
"AHDR -- animation header"
|
||||
|
||||
# assertions
|
||||
if self.count != 0:
|
||||
raise SyntaxError("misplaced AHDR chunk")
|
||||
|
||||
s = self.fp.read(bytes)
|
||||
self.size = i32(s), i32(s[4:])
|
||||
try:
|
||||
self.mode, self.rawmode = _MODES[(i8(s[8]), i8(s[9]))]
|
||||
except:
|
||||
raise SyntaxError("unknown ARG mode")
|
||||
|
||||
if Image.DEBUG:
|
||||
print("AHDR size", self.size)
|
||||
print("AHDR mode", self.mode, self.rawmode)
|
||||
|
||||
return s
|
||||
|
||||
def chunk_AFRM(self, offset, bytes):
|
||||
"AFRM -- next frame follows"
|
||||
|
||||
# assertions
|
||||
if self.count != 0:
|
||||
raise SyntaxError("misplaced AFRM chunk")
|
||||
|
||||
self.show = 1
|
||||
self.id = 0
|
||||
self.count = 1
|
||||
self.repair = None
|
||||
|
||||
s = self.fp.read(bytes)
|
||||
if len(s) >= 2:
|
||||
self.id = i16(s)
|
||||
if len(s) >= 4:
|
||||
self.count = i16(s[2:4])
|
||||
if len(s) >= 6:
|
||||
self.repair = i16(s[4:6])
|
||||
else:
|
||||
self.repair = None
|
||||
|
||||
if Image.DEBUG:
|
||||
print("AFRM", self.id, self.count)
|
||||
|
||||
return s
|
||||
|
||||
def chunk_ADEF(self, offset, bytes):
|
||||
"ADEF -- store image"
|
||||
|
||||
# assertions
|
||||
if self.count != 0:
|
||||
raise SyntaxError("misplaced ADEF chunk")
|
||||
|
||||
self.show = 0
|
||||
self.id = 0
|
||||
self.count = 1
|
||||
self.repair = None
|
||||
|
||||
s = self.fp.read(bytes)
|
||||
if len(s) >= 2:
|
||||
self.id = i16(s)
|
||||
if len(s) >= 4:
|
||||
self.count = i16(s[2:4])
|
||||
|
||||
if Image.DEBUG:
|
||||
print("ADEF", self.id, self.count)
|
||||
|
||||
return s
|
||||
|
||||
def chunk_NAME(self, offset, bytes):
|
||||
"NAME -- name the current image"
|
||||
|
||||
# assertions
|
||||
if self.count == 0:
|
||||
raise SyntaxError("misplaced NAME chunk")
|
||||
|
||||
name = self.fp.read(bytes)
|
||||
self.names[self.id] = name
|
||||
|
||||
return name
|
||||
|
||||
def chunk_AEND(self, offset, bytes):
|
||||
"AEND -- end of animation"
|
||||
|
||||
if Image.DEBUG:
|
||||
print("AEND")
|
||||
|
||||
self.eof = 1
|
||||
|
||||
raise EOFError("end of ARG file")
|
||||
|
||||
def __getmodesize(self, s, full=1):
|
||||
|
||||
size = i32(s), i32(s[4:])
|
||||
|
||||
try:
|
||||
mode, rawmode = _MODES[(i8(s[8]), i8(s[9]))]
|
||||
except:
|
||||
raise SyntaxError("unknown image mode")
|
||||
|
||||
if full:
|
||||
if i8(s[12]):
|
||||
pass # interlace not yet supported
|
||||
if i8(s[11]):
|
||||
raise SyntaxError("unknown filter category")
|
||||
|
||||
return size, mode, rawmode
|
||||
|
||||
def chunk_PAST(self, offset, bytes):
|
||||
"PAST -- paste one image into another"
|
||||
|
||||
# assertions
|
||||
if self.count == 0:
|
||||
raise SyntaxError("misplaced PAST chunk")
|
||||
|
||||
if self.repair is not None:
|
||||
# we must repair the target image before we
|
||||
# start pasting
|
||||
|
||||
# brute force; a better solution would be to
|
||||
# update only the dirty rectangles in images[id].
|
||||
# note that if images[id] doesn't exist, it must
|
||||
# be created
|
||||
|
||||
self.images[self.id] = self.images[self.repair].copy()
|
||||
self.repair = None
|
||||
|
||||
s = self.fp.read(bytes)
|
||||
im = self.images[i16(s)]
|
||||
x, y = i32(s[2:6]), i32(s[6:10])
|
||||
bbox = x, y, im.size[0]+x, im.size[1]+y
|
||||
|
||||
if im.mode in ["RGBA"]:
|
||||
# paste with transparency
|
||||
# FIXME: should handle P+transparency as well
|
||||
self.images[self.id].paste(im, bbox, im)
|
||||
else:
|
||||
# paste without transparency
|
||||
self.images[self.id].paste(im, bbox)
|
||||
|
||||
self.action = ("PAST",)
|
||||
self.__store()
|
||||
|
||||
return s
|
||||
|
||||
def chunk_BLNK(self, offset, bytes):
|
||||
"BLNK -- create blank image"
|
||||
|
||||
# assertions
|
||||
if self.count == 0:
|
||||
raise SyntaxError("misplaced BLNK chunk")
|
||||
|
||||
s = self.fp.read(bytes)
|
||||
size, mode, rawmode = self.__getmodesize(s, 0)
|
||||
|
||||
# store image (FIXME: handle colour)
|
||||
self.action = ("BLNK",)
|
||||
self.im = Image.core.fill(mode, size, 0)
|
||||
self.__store()
|
||||
|
||||
return s
|
||||
|
||||
def chunk_IHDR(self, offset, bytes):
|
||||
"IHDR -- full image follows"
|
||||
|
||||
# assertions
|
||||
if self.count == 0:
|
||||
raise SyntaxError("misplaced IHDR chunk")
|
||||
|
||||
# image header
|
||||
s = self.fp.read(bytes)
|
||||
size, mode, rawmode = self.__getmodesize(s)
|
||||
|
||||
# decode and store image
|
||||
self.action = ("IHDR",)
|
||||
self.im = Image.core.new(mode, size)
|
||||
self.decoder = Image.core.zip_decoder(rawmode)
|
||||
self.decoder.setimage(self.im, (0,0) + size)
|
||||
self.data = b""
|
||||
|
||||
return s
|
||||
|
||||
def chunk_DHDR(self, offset, bytes):
|
||||
"DHDR -- delta image follows"
|
||||
|
||||
# assertions
|
||||
if self.count == 0:
|
||||
raise SyntaxError("misplaced DHDR chunk")
|
||||
|
||||
s = self.fp.read(bytes)
|
||||
|
||||
size, mode, rawmode = self.__getmodesize(s)
|
||||
|
||||
# delta header
|
||||
diff = i8(s[13])
|
||||
offs = i32(s[14:18]), i32(s[18:22])
|
||||
|
||||
bbox = offs + (offs[0]+size[0], offs[1]+size[1])
|
||||
|
||||
if Image.DEBUG:
|
||||
print("DHDR", diff, bbox)
|
||||
|
||||
# FIXME: decode and apply image
|
||||
self.action = ("DHDR", diff, bbox)
|
||||
|
||||
# setup decoder
|
||||
self.im = Image.core.new(mode, size)
|
||||
|
||||
self.decoder = Image.core.zip_decoder(rawmode)
|
||||
self.decoder.setimage(self.im, (0,0) + size)
|
||||
|
||||
self.data = b""
|
||||
|
||||
return s
|
||||
|
||||
def chunk_JHDR(self, offset, bytes):
|
||||
"JHDR -- JPEG image follows"
|
||||
|
||||
# assertions
|
||||
if self.count == 0:
|
||||
raise SyntaxError("misplaced JHDR chunk")
|
||||
|
||||
# image header
|
||||
s = self.fp.read(bytes)
|
||||
size, mode, rawmode = self.__getmodesize(s, 0)
|
||||
|
||||
# decode and store image
|
||||
self.action = ("JHDR",)
|
||||
self.im = Image.core.new(mode, size)
|
||||
self.decoder = Image.core.jpeg_decoder(rawmode)
|
||||
self.decoder.setimage(self.im, (0,0) + size)
|
||||
self.data = b""
|
||||
|
||||
return s
|
||||
|
||||
def chunk_UHDR(self, offset, bytes):
|
||||
"UHDR -- uncompressed image data follows (EXPERIMENTAL)"
|
||||
|
||||
# assertions
|
||||
if self.count == 0:
|
||||
raise SyntaxError("misplaced UHDR chunk")
|
||||
|
||||
# image header
|
||||
s = self.fp.read(bytes)
|
||||
size, mode, rawmode = self.__getmodesize(s, 0)
|
||||
|
||||
# decode and store image
|
||||
self.action = ("UHDR",)
|
||||
self.im = Image.core.new(mode, size)
|
||||
self.decoder = Image.core.raw_decoder(rawmode)
|
||||
self.decoder.setimage(self.im, (0,0) + size)
|
||||
self.data = b""
|
||||
|
||||
return s
|
||||
|
||||
def chunk_IDAT(self, offset, bytes):
|
||||
"IDAT -- image data block"
|
||||
|
||||
# pass compressed chunks through the decoder
|
||||
s = self.fp.read(bytes)
|
||||
self.data = self.data + s
|
||||
n, e = self.decoder.decode(self.data)
|
||||
if n < 0:
|
||||
# end of image
|
||||
if e < 0:
|
||||
raise IOError("decoder error %d" % e)
|
||||
else:
|
||||
self.data = self.data[n:]
|
||||
|
||||
return s
|
||||
|
||||
def chunk_DEND(self, offset, bytes):
|
||||
return self.chunk_IEND(offset, bytes)
|
||||
|
||||
def chunk_JEND(self, offset, bytes):
|
||||
return self.chunk_IEND(offset, bytes)
|
||||
|
||||
def chunk_UEND(self, offset, bytes):
|
||||
return self.chunk_IEND(offset, bytes)
|
||||
|
||||
def chunk_IEND(self, offset, bytes):
|
||||
"IEND -- end of image"
|
||||
|
||||
# we now have a new image. carry out the operation
|
||||
# defined by the image header.
|
||||
|
||||
# won't need these anymore
|
||||
del self.decoder
|
||||
del self.data
|
||||
|
||||
self.__store()
|
||||
|
||||
return self.fp.read(bytes)
|
||||
|
||||
def __store(self):
|
||||
|
||||
# apply operation
|
||||
cid = self.action[0]
|
||||
|
||||
if cid in ["BLNK", "IHDR", "JHDR", "UHDR"]:
|
||||
# store
|
||||
self.images[self.id] = self.im
|
||||
|
||||
elif cid == "DHDR":
|
||||
# paste
|
||||
cid, mode, bbox = self.action
|
||||
im0 = self.images[self.id]
|
||||
im1 = self.im
|
||||
if mode == 0:
|
||||
im1 = im1.chop_add_modulo(im0.crop(bbox))
|
||||
im0.paste(im1, bbox)
|
||||
|
||||
self.count = self.count - 1
|
||||
|
||||
if self.count == 0 and self.show:
|
||||
self.im = self.images[self.id]
|
||||
raise EOFError # end of this frame
|
||||
|
||||
def chunk_PLTE(self, offset, bytes):
|
||||
"PLTE -- palette data"
|
||||
|
||||
s = self.fp.read(bytes)
|
||||
if self.mode == "P":
|
||||
self.palette = ImagePalette.raw("RGB", s)
|
||||
return s
|
||||
|
||||
def chunk_sYNC(self, offset, bytes):
|
||||
"SYNC -- reset decoder"
|
||||
|
||||
if self.count != 0:
|
||||
raise SyntaxError("misplaced sYNC chunk")
|
||||
|
||||
s = self.fp.read(bytes)
|
||||
self.__reset()
|
||||
return s
|
||||
|
||||
|
||||
# --------------------------------------------------------------------
|
||||
# ARG reader
|
||||
|
||||
def _accept(prefix):
|
||||
return prefix[:8] == MAGIC
|
||||
|
||||
##
|
||||
# Image plugin for the experimental Animated Raster Graphics format.
|
||||
|
||||
class ArgImageFile(ImageFile.ImageFile):
|
||||
|
||||
format = "ARG"
|
||||
format_description = "Animated raster graphics"
|
||||
|
||||
def _open(self):
|
||||
|
||||
if Image.warnings:
|
||||
Image.warnings.warn(
|
||||
"The ArgImagePlugin driver is obsolete, and will be removed "
|
||||
"from a future release of PIL. If you rely on this module, "
|
||||
"please contact the PIL authors.",
|
||||
RuntimeWarning
|
||||
)
|
||||
|
||||
if self.fp.read(8) != MAGIC:
|
||||
raise SyntaxError("not an ARG file")
|
||||
|
||||
self.arg = ArgStream(self.fp)
|
||||
|
||||
# read and process the first chunk (AHDR)
|
||||
|
||||
cid, offset, bytes = self.arg.read()
|
||||
|
||||
if cid != "AHDR":
|
||||
raise SyntaxError("expected an AHDR chunk")
|
||||
|
||||
s = self.arg.call(cid, offset, bytes)
|
||||
|
||||
self.arg.crc(cid, s)
|
||||
|
||||
# image characteristics
|
||||
self.mode = self.arg.mode
|
||||
self.size = self.arg.size
|
||||
|
||||
def load(self):
|
||||
|
||||
if self.arg.im is None:
|
||||
self.seek(0)
|
||||
|
||||
# image data
|
||||
self.im = self.arg.im
|
||||
self.palette = self.arg.palette
|
||||
|
||||
# set things up for further processing
|
||||
Image.Image.load(self)
|
||||
|
||||
def seek(self, frame):
|
||||
|
||||
if self.arg.eof:
|
||||
raise EOFError("end of animation")
|
||||
|
||||
self.fp = self.arg.fp
|
||||
|
||||
while True:
|
||||
|
||||
#
|
||||
# process chunks
|
||||
|
||||
cid, offset, bytes = self.arg.read()
|
||||
|
||||
if self.arg.eof:
|
||||
raise EOFError("end of animation")
|
||||
|
||||
try:
|
||||
s = self.arg.call(cid, offset, bytes)
|
||||
except EOFError:
|
||||
break
|
||||
|
||||
except "glurk": # AttributeError
|
||||
if Image.DEBUG:
|
||||
print(cid, bytes, "(unknown)")
|
||||
s = self.fp.read(bytes)
|
||||
|
||||
self.arg.crc(cid, s)
|
||||
|
||||
self.fp.read(4) # ship extra CRC
|
||||
|
||||
def tell(self):
|
||||
return 0
|
||||
|
||||
def verify(self):
|
||||
"Verify ARG file"
|
||||
|
||||
# back up to first chunk
|
||||
self.fp.seek(8)
|
||||
|
||||
self.arg.verify(self)
|
||||
self.arg.close()
|
||||
|
||||
self.fp = None
|
||||
|
||||
#
|
||||
# --------------------------------------------------------------------
|
||||
|
||||
Image.register_open("ARG", ArgImageFile, _accept)
|
||||
|
||||
Image.register_extension("ARG", ".arg")
|
||||
|
||||
Image.register_mime("ARG", "video/x-arg")
|
|
@ -128,5 +128,5 @@ class BdfFontFile(FontFile.FontFile):
|
|||
if not c:
|
||||
break
|
||||
id, ch, (xy, dst, src), im = c
|
||||
if ch >= 0 and ch < len(self.glyph):
|
||||
if 0 <= ch < len(self.glyph):
|
||||
self.glyph[ch] = xy, dst, src, im
|
||||
|
|
|
@ -28,6 +28,7 @@ __version__ = "0.7"
|
|||
|
||||
|
||||
from PIL import Image, ImageFile, ImagePalette, _binary
|
||||
import math
|
||||
|
||||
i8 = _binary.i8
|
||||
i16 = _binary.i16le
|
||||
|
@ -88,6 +89,7 @@ class BmpImageFile(ImageFile.ImageFile):
|
|||
bits = i16(s[14:])
|
||||
self.size = i32(s[4:]), i32(s[8:])
|
||||
compression = i32(s[16:])
|
||||
pxperm = (i32(s[24:]), i32(s[28:])) # Pixels per meter
|
||||
lutsize = 4
|
||||
colors = i32(s[32:])
|
||||
direction = -1
|
||||
|
@ -95,6 +97,8 @@ class BmpImageFile(ImageFile.ImageFile):
|
|||
# upside-down storage
|
||||
self.size = self.size[0], 2**32 - self.size[1]
|
||||
direction = 0
|
||||
|
||||
self.info["dpi"] = tuple(map(lambda x: math.ceil(x / 39.3701), pxperm))
|
||||
|
||||
else:
|
||||
raise IOError("Unsupported BMP header type (%d)" % len(s))
|
||||
|
@ -203,30 +207,37 @@ def _save(im, fp, filename, check=0):
|
|||
if check:
|
||||
return check
|
||||
|
||||
info = im.encoderinfo
|
||||
|
||||
dpi = info.get("dpi", (96, 96))
|
||||
|
||||
# 1 meter == 39.3701 inches
|
||||
ppm = tuple(map(lambda x: int(x * 39.3701), dpi))
|
||||
|
||||
stride = ((im.size[0]*bits+7)//8+3)&(~3)
|
||||
header = 40 # or 64 for OS/2 version 2
|
||||
offset = 14 + header + colors * 4
|
||||
image = stride * im.size[1]
|
||||
|
||||
# bitmap header
|
||||
fp.write(b"BM" + # file type (magic)
|
||||
o32(offset+image) + # file size
|
||||
o32(0) + # reserved
|
||||
o32(offset)) # image data offset
|
||||
fp.write(b"BM" + # file type (magic)
|
||||
o32(offset+image) + # file size
|
||||
o32(0) + # reserved
|
||||
o32(offset)) # image data offset
|
||||
|
||||
# bitmap info header
|
||||
fp.write(o32(header) + # info header size
|
||||
o32(im.size[0]) + # width
|
||||
o32(im.size[1]) + # height
|
||||
o16(1) + # planes
|
||||
o16(bits) + # depth
|
||||
o32(0) + # compression (0=uncompressed)
|
||||
o32(image) + # size of bitmap
|
||||
o32(1) + o32(1) + # resolution
|
||||
o32(colors) + # colors used
|
||||
o32(colors)) # colors important
|
||||
fp.write(o32(header) + # info header size
|
||||
o32(im.size[0]) + # width
|
||||
o32(im.size[1]) + # height
|
||||
o16(1) + # planes
|
||||
o16(bits) + # depth
|
||||
o32(0) + # compression (0=uncompressed)
|
||||
o32(image) + # size of bitmap
|
||||
o32(ppm[0]) + o32(ppm[1]) + # resolution
|
||||
o32(colors) + # colors used
|
||||
o32(colors)) # colors important
|
||||
|
||||
fp.write(b"\0" * (header - 40)) # padding (for OS/2 format)
|
||||
fp.write(b"\0" * (header - 40)) # padding (for OS/2 format)
|
||||
|
||||
if im.mode == "1":
|
||||
for i in (0, 255):
|
||||
|
|
|
@ -33,6 +33,7 @@ i32 = _binary.i32le
|
|||
def _accept(prefix):
|
||||
return prefix[:4] == b"\0\0\2\0"
|
||||
|
||||
|
||||
##
|
||||
# Image plugin for Windows Cursor files.
|
||||
|
||||
|
@ -48,7 +49,7 @@ class CurImageFile(BmpImagePlugin.BmpImageFile):
|
|||
# check magic
|
||||
s = self.fp.read(6)
|
||||
if not _accept(s):
|
||||
raise SyntaxError("not an CUR file")
|
||||
raise SyntaxError("not a CUR file")
|
||||
|
||||
# pick the largest cursor in the file
|
||||
m = b""
|
||||
|
@ -58,14 +59,14 @@ class CurImageFile(BmpImagePlugin.BmpImageFile):
|
|||
m = s
|
||||
elif i8(s[0]) > i8(m[0]) and i8(s[1]) > i8(m[1]):
|
||||
m = s
|
||||
#print "width", i8(s[0])
|
||||
#print "height", i8(s[1])
|
||||
#print "colors", i8(s[2])
|
||||
#print "reserved", i8(s[3])
|
||||
#print "hotspot x", i16(s[4:])
|
||||
#print "hotspot y", i16(s[6:])
|
||||
#print "bytes", i32(s[8:])
|
||||
#print "offset", i32(s[12:])
|
||||
# print "width", i8(s[0])
|
||||
# print "height", i8(s[1])
|
||||
# print "colors", i8(s[2])
|
||||
# print "reserved", i8(s[3])
|
||||
# print "hotspot x", i16(s[4:])
|
||||
# print "hotspot y", i16(s[6:])
|
||||
# print "bytes", i32(s[8:])
|
||||
# print "offset", i32(s[12:])
|
||||
|
||||
# load as bitmap
|
||||
self._bitmap(i32(m[12:]) + offset)
|
||||
|
@ -73,7 +74,7 @@ class CurImageFile(BmpImagePlugin.BmpImageFile):
|
|||
# patch up the bitmap height
|
||||
self.size = self.size[0], self.size[1]//2
|
||||
d, e, o, a = self.tile[0]
|
||||
self.tile[0] = d, (0,0)+self.size, o, a
|
||||
self.tile[0] = d, (0, 0)+self.size, o, a
|
||||
|
||||
return
|
||||
|
||||
|
|
|
@ -27,13 +27,15 @@ from PIL import Image, _binary
|
|||
|
||||
from PIL.PcxImagePlugin import PcxImageFile
|
||||
|
||||
MAGIC = 0x3ADE68B1 # QUIZ: what's this value, then?
|
||||
MAGIC = 0x3ADE68B1 # QUIZ: what's this value, then?
|
||||
|
||||
i32 = _binary.i32le
|
||||
|
||||
|
||||
def _accept(prefix):
|
||||
return i32(prefix) == MAGIC
|
||||
|
||||
|
||||
##
|
||||
# Image plugin for the Intel DCX format.
|
||||
|
||||
|
|
|
@ -104,7 +104,7 @@ def Ghostscript(tile, size, fp, scale=1):
|
|||
s = fp.read(min(lengthfile, 100*1024))
|
||||
if not s:
|
||||
break
|
||||
lengthfile = lengthfile - len(s)
|
||||
length -= len(s)
|
||||
f.write(s)
|
||||
|
||||
# Build ghostscript command
|
||||
|
@ -161,7 +161,7 @@ class PSFile:
|
|||
def tell(self):
|
||||
pos = self.fp.tell()
|
||||
if self.char:
|
||||
pos = pos - 1
|
||||
pos -= 1
|
||||
return pos
|
||||
def readline(self):
|
||||
s = b""
|
||||
|
|
|
@ -63,15 +63,12 @@ TAGS = {
|
|||
0x0201: "JpegIFOffset",
|
||||
0x0202: "JpegIFByteCount",
|
||||
0x0211: "YCbCrCoefficients",
|
||||
0x0211: "YCbCrCoefficients",
|
||||
0x0212: "YCbCrSubSampling",
|
||||
0x0213: "YCbCrPositioning",
|
||||
0x0213: "YCbCrPositioning",
|
||||
0x0214: "ReferenceBlackWhite",
|
||||
0x0214: "ReferenceBlackWhite",
|
||||
0x1000: "RelatedImageFileFormat",
|
||||
0x1001: "RelatedImageLength",
|
||||
0x1001: "RelatedImageWidth",
|
||||
0x1001: "RelatedImageLength", # FIXME / Dictionary contains duplicate keys
|
||||
0x1001: "RelatedImageWidth", # FIXME \ Dictionary contains duplicate keys
|
||||
0x828d: "CFARepeatPatternDim",
|
||||
0x828e: "CFAPattern",
|
||||
0x828f: "BatteryLevel",
|
||||
|
|
|
@ -105,7 +105,7 @@ class FliImageFile(ImageFile.ImageFile):
|
|||
g = i8(s[n+1]) << shift
|
||||
b = i8(s[n+2]) << shift
|
||||
palette[i] = (r, g, b)
|
||||
i = i + 1
|
||||
i += 1
|
||||
|
||||
def seek(self, frame):
|
||||
|
||||
|
|
|
@ -17,8 +17,6 @@
|
|||
import os
|
||||
from PIL import Image, _binary
|
||||
|
||||
import marshal
|
||||
|
||||
try:
|
||||
import zlib
|
||||
except ImportError:
|
||||
|
@ -26,13 +24,15 @@ except ImportError:
|
|||
|
||||
WIDTH = 800
|
||||
|
||||
|
||||
def puti16(fp, values):
|
||||
# write network order (big-endian) 16-bit sequence
|
||||
for v in values:
|
||||
if v < 0:
|
||||
v = v + 65536
|
||||
v += 65536
|
||||
fp.write(_binary.o16be(v))
|
||||
|
||||
|
||||
##
|
||||
# Base class for raster font file handlers.
|
||||
|
||||
|
@ -63,7 +63,7 @@ class FontFile:
|
|||
h = max(h, src[3] - src[1])
|
||||
w = w + (src[2] - src[0])
|
||||
if w > WIDTH:
|
||||
lines = lines + 1
|
||||
lines += 1
|
||||
w = (src[2] - src[0])
|
||||
maxwidth = max(maxwidth, w)
|
||||
|
||||
|
@ -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
|
||||
|
|
|
@ -84,7 +84,7 @@ class FpxImageFile(ImageFile.ImageFile):
|
|||
i = 1
|
||||
while size > 64:
|
||||
size = size / 2
|
||||
i = i + 1
|
||||
i += 1
|
||||
self.maxid = i - 1
|
||||
|
||||
# mode. instead of using a single field for this, flashpix
|
||||
|
|
|
@ -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
|
||||
|
||||
|
@ -333,13 +362,41 @@ def _save_netpbm(im, fp, filename):
|
|||
# below for information on how to enable this.
|
||||
|
||||
import os
|
||||
from subprocess import Popen, check_call, PIPE, CalledProcessError
|
||||
import tempfile
|
||||
file = im._dump()
|
||||
|
||||
if im.mode != "RGB":
|
||||
os.system("ppmtogif %s >%s" % (file, filename))
|
||||
with open(filename, 'wb') as f:
|
||||
stderr = tempfile.TemporaryFile()
|
||||
check_call(["ppmtogif", file], stdout=f, stderr=stderr)
|
||||
else:
|
||||
os.system("ppmquant 256 %s | ppmtogif >%s" % (file, filename))
|
||||
try: os.unlink(file)
|
||||
except: pass
|
||||
with open(filename, 'wb') as f:
|
||||
|
||||
# Pipe ppmquant output into ppmtogif
|
||||
# "ppmquant 256 %s | ppmtogif > %s" % (file, filename)
|
||||
quant_cmd = ["ppmquant", "256", file]
|
||||
togif_cmd = ["ppmtogif"]
|
||||
stderr = tempfile.TemporaryFile()
|
||||
quant_proc = Popen(quant_cmd, stdout=PIPE, stderr=stderr)
|
||||
stderr = tempfile.TemporaryFile()
|
||||
togif_proc = Popen(togif_cmd, stdin=quant_proc.stdout, stdout=f, stderr=stderr)
|
||||
|
||||
# Allow ppmquant to receive SIGPIPE if ppmtogif exits
|
||||
quant_proc.stdout.close()
|
||||
|
||||
retcode = quant_proc.wait()
|
||||
if retcode:
|
||||
raise CalledProcessError(retcode, quant_cmd)
|
||||
|
||||
retcode = togif_proc.wait()
|
||||
if retcode:
|
||||
raise CalledProcessError(retcode, togif_cmd)
|
||||
|
||||
try:
|
||||
os.unlink(file)
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
# --------------------------------------------------------------------
|
||||
|
|
|
@ -68,7 +68,7 @@ class GradientFile:
|
|||
x = i / float(entries-1)
|
||||
|
||||
while x1 < x:
|
||||
ix = ix + 1
|
||||
ix += 1
|
||||
x0, x1, xm, rgb0, rgb1, segment = self.gradient[ix]
|
||||
|
||||
w = x1 - x0
|
||||
|
|
|
@ -52,7 +52,7 @@ class GimpPaletteFile:
|
|||
if 0 <= i <= 255:
|
||||
self.palette[i] = o8(v[0]) + o8(v[1]) + o8(v[2])
|
||||
|
||||
i = i + 1
|
||||
i += 1
|
||||
|
||||
self.palette = b"".join(self.palette)
|
||||
|
||||
|
|
|
@ -70,7 +70,7 @@ def read_32(fobj, start_length, size):
|
|||
else:
|
||||
blocksize = byte + 1
|
||||
data.append(fobj.read(blocksize))
|
||||
bytesleft = bytesleft - blocksize
|
||||
bytesleft -= blocksize
|
||||
if bytesleft <= 0:
|
||||
break
|
||||
if bytesleft != 0:
|
||||
|
@ -179,11 +179,13 @@ class IcnsFile:
|
|||
i = HEADERSIZE
|
||||
while i < filesize:
|
||||
sig, blocksize = nextheader(fobj)
|
||||
i = i + HEADERSIZE
|
||||
blocksize = blocksize - HEADERSIZE
|
||||
if blocksize <= 0:
|
||||
raise SyntaxError('invalid block header')
|
||||
i += HEADERSIZE
|
||||
blocksize -= HEADERSIZE
|
||||
dct[sig] = (i, blocksize)
|
||||
fobj.seek(blocksize, 1)
|
||||
i = i + blocksize
|
||||
i += blocksize
|
||||
|
||||
def itersizes(self):
|
||||
sizes = []
|
||||
|
|
|
@ -182,7 +182,7 @@ class ImImageFile(ImageFile.ImageFile):
|
|||
self.info[k] = v
|
||||
|
||||
if k in TAGS:
|
||||
n = n + 1
|
||||
n += 1
|
||||
|
||||
else:
|
||||
|
||||
|
|
41
PIL/Image.py
|
@ -31,11 +31,18 @@ from PIL import VERSION, PILLOW_VERSION, _plugins
|
|||
import warnings
|
||||
|
||||
|
||||
class DecompressionBombWarning(RuntimeWarning):
|
||||
pass
|
||||
|
||||
class _imaging_not_installed:
|
||||
# module placeholder
|
||||
def __getattr__(self, id):
|
||||
raise ImportError("The _imaging C module is not installed")
|
||||
|
||||
|
||||
# Limit to around a quarter gigabyte for a 24 bit (3 bpp) image
|
||||
MAX_IMAGE_PIXELS = int(1024 * 1024 * 1024 / 4 / 3)
|
||||
|
||||
try:
|
||||
# give Tk a chance to set up the environment, in case we're
|
||||
# using an _imaging module linked against libtcl/libtk (use
|
||||
|
@ -213,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
|
||||
|
@ -448,7 +456,7 @@ def _getscaleoffset(expr):
|
|||
(a, b, c) = data # simplified syntax
|
||||
if (a is stub and b == "__mul__" and isinstance(c, numbers.Number)):
|
||||
return c, 0.0
|
||||
if (a is stub and b == "__add__" and isinstance(c, numbers.Number)):
|
||||
if a is stub and b == "__add__" and isinstance(c, numbers.Number):
|
||||
return 1.0, c
|
||||
except TypeError:
|
||||
pass
|
||||
|
@ -532,7 +540,7 @@ class Image:
|
|||
try:
|
||||
self.fp.close()
|
||||
except Exception as msg:
|
||||
if Image.DEBUG:
|
||||
if DEBUG:
|
||||
print ("Error closing: %s" % msg)
|
||||
|
||||
# Instead of simply setting to None, we're setting up a
|
||||
|
@ -547,7 +555,6 @@ class Image:
|
|||
self.readonly = 0
|
||||
|
||||
def _dump(self, file=None, format=None):
|
||||
import os
|
||||
import tempfile
|
||||
suffix = ''
|
||||
if format:
|
||||
|
@ -566,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())
|
||||
|
@ -1529,7 +1538,7 @@ class Image:
|
|||
clockwise around its centre.
|
||||
|
||||
:param angle: In degrees counter clockwise.
|
||||
:param filter: An optional resampling filter. This can be
|
||||
:param resample: An optional resampling filter. This can be
|
||||
one of :py:attr:`PIL.Image.NEAREST` (use nearest neighbour),
|
||||
:py:attr:`PIL.Image.BILINEAR` (linear interpolation in a 2x2
|
||||
environment), or :py:attr:`PIL.Image.BICUBIC`
|
||||
|
@ -1550,7 +1559,6 @@ class Image:
|
|||
math.cos(angle), math.sin(angle), 0.0,
|
||||
-math.sin(angle), math.cos(angle), 0.0
|
||||
]
|
||||
|
||||
|
||||
def transform(x, y, matrix=matrix):
|
||||
(a, b, c, d, e, f) = matrix
|
||||
|
@ -2075,7 +2083,6 @@ def frombuffer(mode, size, data, decoder_name="raw", *args):
|
|||
|
||||
.. versionadded:: 1.1.4
|
||||
"""
|
||||
"Load image from bytes or buffer"
|
||||
|
||||
# may pass tuple instead of argument list
|
||||
if len(args) == 1 and isinstance(args[0], tuple):
|
||||
|
@ -2173,6 +2180,20 @@ _fromarray_typemap[((1, 1), _ENDIAN + "i4")] = ("I", "I")
|
|||
_fromarray_typemap[((1, 1), _ENDIAN + "f4")] = ("F", "F")
|
||||
|
||||
|
||||
def _decompression_bomb_check(size):
|
||||
if MAX_IMAGE_PIXELS is None:
|
||||
return
|
||||
|
||||
pixels = size[0] * size[1]
|
||||
|
||||
if pixels > MAX_IMAGE_PIXELS:
|
||||
warnings.warn(
|
||||
"Image size (%d pixels) exceeds limit of %d pixels, "
|
||||
"could be decompression bomb DOS attack." %
|
||||
(pixels, MAX_IMAGE_PIXELS),
|
||||
DecompressionBombWarning)
|
||||
|
||||
|
||||
def open(fp, mode="r"):
|
||||
"""
|
||||
Opens and identifies the given image file.
|
||||
|
@ -2210,7 +2231,9 @@ def open(fp, mode="r"):
|
|||
factory, accept = OPEN[i]
|
||||
if not accept or accept(prefix):
|
||||
fp.seek(0)
|
||||
return factory(fp, filename)
|
||||
im = factory(fp, filename)
|
||||
_decompression_bomb_check(im.size)
|
||||
return im
|
||||
except (SyntaxError, IndexError, TypeError):
|
||||
# import traceback
|
||||
# traceback.print_exc()
|
||||
|
@ -2223,7 +2246,9 @@ def open(fp, mode="r"):
|
|||
factory, accept = OPEN[i]
|
||||
if not accept or accept(prefix):
|
||||
fp.seek(0)
|
||||
return factory(fp, filename)
|
||||
im = factory(fp, filename)
|
||||
_decompression_bomb_check(im.size)
|
||||
return im
|
||||
except (SyntaxError, IndexError, TypeError):
|
||||
# import traceback
|
||||
# traceback.print_exc()
|
||||
|
|
415
PIL/ImageCms.py
|
@ -1,19 +1,19 @@
|
|||
#
|
||||
# The Python Imaging Library.
|
||||
# $Id$
|
||||
#
|
||||
# optional color managment support, based on Kevin Cazabon's PyCMS
|
||||
# library.
|
||||
#
|
||||
# History:
|
||||
# 2009-03-08 fl Added to PIL.
|
||||
#
|
||||
# Copyright (C) 2002-2003 Kevin Cazabon
|
||||
# Copyright (c) 2009 by Fredrik Lundh
|
||||
#
|
||||
# See the README file for information on usage and redistribution. See
|
||||
# below for the original description.
|
||||
#
|
||||
## The Python Imaging Library.
|
||||
## $Id$
|
||||
|
||||
## Optional color managment support, based on Kevin Cazabon's PyCMS
|
||||
## library.
|
||||
|
||||
## History:
|
||||
|
||||
## 2009-03-08 fl Added to PIL.
|
||||
|
||||
## 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
|
||||
|
||||
|
@ -66,7 +66,8 @@ pyCMS
|
|||
|
||||
Added try/except statements arount type() checks of
|
||||
potential CObjects... Python won't let you use type()
|
||||
on them, and raises a TypeError (stupid, if you ask me!)
|
||||
on them, and raises a TypeError (stupid, if you ask
|
||||
me!)
|
||||
|
||||
Added buildProofTransformFromOpenProfiles() function.
|
||||
Additional fixes in DLL, see DLL code for details.
|
||||
|
@ -88,9 +89,9 @@ try:
|
|||
from PIL import _imagingcms
|
||||
except ImportError as ex:
|
||||
# Allow error import for doc purposes, but error out when accessing
|
||||
# anything in core.
|
||||
from _util import deferred_error
|
||||
_imagingcms = deferred_error(ex)
|
||||
# anything in core.
|
||||
from _util import import_err
|
||||
_imagingcms = import_err(ex)
|
||||
from PIL._util import isStringType
|
||||
|
||||
core = _imagingcms
|
||||
|
@ -113,22 +114,24 @@ DIRECTION_PROOF = 2
|
|||
FLAGS = {
|
||||
"MATRIXINPUT": 1,
|
||||
"MATRIXOUTPUT": 2,
|
||||
"MATRIXONLY": (1|2),
|
||||
"NOWHITEONWHITEFIXUP": 4, # Don't hot fix scum dot
|
||||
"NOPRELINEARIZATION": 16, # Don't create prelinearization tables on precalculated transforms (internal use)
|
||||
"GUESSDEVICECLASS": 32, # Guess device class (for transform2devicelink)
|
||||
"NOTCACHE": 64, # Inhibit 1-pixel cache
|
||||
"MATRIXONLY": (1 | 2),
|
||||
"NOWHITEONWHITEFIXUP": 4, # Don't hot fix scum dot
|
||||
# Don't create prelinearization tables on precalculated transforms
|
||||
# (internal use):
|
||||
"NOPRELINEARIZATION": 16,
|
||||
"GUESSDEVICECLASS": 32, # Guess device class (for transform2devicelink)
|
||||
"NOTCACHE": 64, # Inhibit 1-pixel cache
|
||||
"NOTPRECALC": 256,
|
||||
"NULLTRANSFORM": 512, # Don't transform anyway
|
||||
"HIGHRESPRECALC": 1024, # Use more memory to give better accurancy
|
||||
"LOWRESPRECALC": 2048, # Use less memory to minimize resouces
|
||||
"NULLTRANSFORM": 512, # Don't transform anyway
|
||||
"HIGHRESPRECALC": 1024, # Use more memory to give better accurancy
|
||||
"LOWRESPRECALC": 2048, # Use less memory to minimize resouces
|
||||
"WHITEBLACKCOMPENSATION": 8192,
|
||||
"BLACKPOINTCOMPENSATION": 8192,
|
||||
"GAMUTCHECK": 4096, # Out of Gamut alarm
|
||||
"SOFTPROOFING": 16384, # Do softproofing
|
||||
"PRESERVEBLACK": 32768, # Black preservation
|
||||
"NODEFAULTRESOURCEDEF": 16777216, # CRD special
|
||||
"GRIDPOINTS": lambda n: ((n) & 0xFF) << 16 # Gridpoints
|
||||
"GAMUTCHECK": 4096, # Out of Gamut alarm
|
||||
"SOFTPROOFING": 16384, # Do softproofing
|
||||
"PRESERVEBLACK": 32768, # Black preservation
|
||||
"NODEFAULTRESOURCEDEF": 16777216, # CRD special
|
||||
"GRIDPOINTS": lambda n: ((n) & 0xFF) << 16 # Gridpoints
|
||||
}
|
||||
|
||||
_MAX_FLAG = 0
|
||||
|
@ -136,6 +139,7 @@ for flag in FLAGS.values():
|
|||
if isinstance(flag, int):
|
||||
_MAX_FLAG = _MAX_FLAG | flag
|
||||
|
||||
|
||||
# --------------------------------------------------------------------.
|
||||
# Experimental PIL-level API
|
||||
# --------------------------------------------------------------------.
|
||||
|
@ -146,51 +150,71 @@ 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"):
|
||||
self._set(core.profile_frombytes(profile.read()))
|
||||
else:
|
||||
self._set(profile) # assume it's already a profile
|
||||
self._set(profile) # assume it's already a profile
|
||||
|
||||
def _set(self, profile, filename=None):
|
||||
self.profile = profile
|
||||
self.filename = filename
|
||||
if profile:
|
||||
self.product_name = None #profile.product_name
|
||||
self.product_info = None #profile.product_info
|
||||
self.product_name = None # profile.product_name
|
||||
self.product_info = None # profile.product_info
|
||||
else:
|
||||
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, proof_intent=INTENT_ABSOLUTE_COLORIMETRIC, flags=0):
|
||||
intent=INTENT_PERCEPTUAL, proof=None,
|
||||
proof_intent=INTENT_ABSOLUTE_COLORIMETRIC, flags=0):
|
||||
if proof is None:
|
||||
self.transform = core.buildTransform(
|
||||
input.profile, output.profile,
|
||||
input_mode, output_mode,
|
||||
intent,
|
||||
flags
|
||||
)
|
||||
)
|
||||
else:
|
||||
self.transform = core.buildProofTransform(
|
||||
input.profile, output.profile, proof.profile,
|
||||
input_mode, output_mode,
|
||||
intent, proof_intent,
|
||||
flags
|
||||
)
|
||||
)
|
||||
# Note: inputMode and outputMode are for pyCMS compatibility only
|
||||
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)
|
||||
|
||||
|
@ -198,21 +222,24 @@ class ImageCmsTransform(Image.ImagePointHandler):
|
|||
im.load()
|
||||
if imOut is None:
|
||||
imOut = Image.new(self.output_mode, im.size, None)
|
||||
result = self.transform.apply(im.im.id, imOut.im.id)
|
||||
self.transform.apply(im.im.id, imOut.im.id)
|
||||
imOut.info['icc_profile'] = self.output_profile.tobytes()
|
||||
return imOut
|
||||
|
||||
def apply_in_place(self, im):
|
||||
im.load()
|
||||
if im.mode != self.output_mode:
|
||||
raise ValueError("mode mismatch") # wrong output mode
|
||||
result = self.transform.apply(im.im.id, im.im.id)
|
||||
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
|
||||
|
||||
|
||||
def get_display_profile(handle=None):
|
||||
""" (experimental) Fetches the profile for the current display device.
|
||||
:returns: None if the profile is not known.
|
||||
"""
|
||||
|
||||
|
||||
import sys
|
||||
if sys.platform == "win32":
|
||||
from PIL import ImageWin
|
||||
|
@ -229,15 +256,21 @@ def get_display_profile(handle=None):
|
|||
profile = get()
|
||||
return ImageCmsProfile(profile)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------.
|
||||
# pyCMS compatible layer
|
||||
# --------------------------------------------------------------------.
|
||||
|
||||
class PyCMSError(Exception):
|
||||
""" (pyCMS) Exception class. This is used for all errors in the pyCMS API. """
|
||||
|
||||
""" (pyCMS) Exception class.
|
||||
This is used for all errors in the pyCMS API. """
|
||||
pass
|
||||
|
||||
def profileToProfile(im, inputProfile, outputProfile, renderingIntent=INTENT_PERCEPTUAL, outputMode=None, inPlace=0, flags=0):
|
||||
|
||||
def profileToProfile(
|
||||
im, inputProfile, outputProfile, renderingIntent=INTENT_PERCEPTUAL,
|
||||
outputMode=None, inPlace=0, flags=0):
|
||||
"""
|
||||
(pyCMS) Applies an ICC transformation to a given image, mapping from
|
||||
inputProfile to outputProfile.
|
||||
|
@ -259,40 +292,45 @@ def profileToProfile(im, inputProfile, outputProfile, renderingIntent=INTENT_PER
|
|||
profiles, the input profile must handle RGB data, and the output
|
||||
profile must handle CMYK data.
|
||||
|
||||
:param im: An open PIL image object (i.e. Image.new(...) or Image.open(...), etc.)
|
||||
:param inputProfile: String, as a valid filename path to the ICC input profile
|
||||
you wish to use for this image, or a profile object
|
||||
:param im: An open PIL image object (i.e. Image.new(...) or
|
||||
Image.open(...), etc.)
|
||||
:param inputProfile: String, as a valid filename path to the ICC input
|
||||
profile you wish to use for this image, or a profile object
|
||||
:param outputProfile: String, as a valid filename path to the ICC output
|
||||
profile you wish to use for this image, or a profile object
|
||||
:param renderingIntent: Integer (0-3) specifying the rendering intent you wish
|
||||
to use for the transform
|
||||
:param renderingIntent: Integer (0-3) specifying the rendering intent you
|
||||
wish to use for the transform
|
||||
|
||||
INTENT_PERCEPTUAL = 0 (DEFAULT) (ImageCms.INTENT_PERCEPTUAL)
|
||||
INTENT_RELATIVE_COLORIMETRIC = 1 (ImageCms.INTENT_RELATIVE_COLORIMETRIC)
|
||||
INTENT_SATURATION = 2 (ImageCms.INTENT_SATURATION)
|
||||
INTENT_ABSOLUTE_COLORIMETRIC = 3 (ImageCms.INTENT_ABSOLUTE_COLORIMETRIC)
|
||||
|
||||
see the pyCMS documentation for details on rendering intents and what they do.
|
||||
:param outputMode: A valid PIL mode for the output image (i.e. "RGB", "CMYK",
|
||||
etc.). Note: if rendering the image "inPlace", outputMode MUST be the
|
||||
same mode as the input, or omitted completely. If omitted, the outputMode
|
||||
will be the same as the mode of the input image (im.mode)
|
||||
:param inPlace: Boolean (1 = True, None or 0 = False). If True, the original
|
||||
image is modified in-place, and None is returned. If False (default), a
|
||||
new Image object is returned with the transform applied.
|
||||
see the pyCMS documentation for details on rendering intents and what
|
||||
they do.
|
||||
:param outputMode: A valid PIL mode for the output image (i.e. "RGB",
|
||||
"CMYK", etc.). Note: if rendering the image "inPlace", outputMode
|
||||
MUST be the same mode as the input, or omitted completely. If
|
||||
omitted, the outputMode will be the same as the mode of the input
|
||||
image (im.mode)
|
||||
:param inPlace: Boolean (1 = True, None or 0 = False). If True, the
|
||||
original image is modified in-place, and None is returned. If False
|
||||
(default), a new Image object is returned with the transform applied.
|
||||
:param flags: Integer (0-...) specifying additional flags
|
||||
:returns: Either None or a new PIL image object, depending on value of inPlace
|
||||
:returns: Either None or a new PIL image object, depending on value of
|
||||
inPlace
|
||||
:exception PyCMSError:
|
||||
"""
|
||||
|
||||
if outputMode is None:
|
||||
outputMode = im.mode
|
||||
|
||||
if not isinstance(renderingIntent, int) or not (0 <= renderingIntent <=3):
|
||||
if not isinstance(renderingIntent, int) or not (0 <= renderingIntent <= 3):
|
||||
raise PyCMSError("renderingIntent must be an integer between 0 and 3")
|
||||
|
||||
if not isinstance(flags, int) or not (0 <= flags <= _MAX_FLAG):
|
||||
raise PyCMSError("flags must be an integer between 0 and %s" + _MAX_FLAG)
|
||||
raise PyCMSError(
|
||||
"flags must be an integer between 0 and %s" + _MAX_FLAG)
|
||||
|
||||
try:
|
||||
if not isinstance(inputProfile, ImageCmsProfile):
|
||||
|
@ -300,8 +338,9 @@ def profileToProfile(im, inputProfile, outputProfile, renderingIntent=INTENT_PER
|
|||
if not isinstance(outputProfile, ImageCmsProfile):
|
||||
outputProfile = ImageCmsProfile(outputProfile)
|
||||
transform = ImageCmsTransform(
|
||||
inputProfile, outputProfile, im.mode, outputMode, renderingIntent, flags=flags
|
||||
)
|
||||
inputProfile, outputProfile, im.mode, outputMode,
|
||||
renderingIntent, flags=flags
|
||||
)
|
||||
if inPlace:
|
||||
transform.apply_in_place(im)
|
||||
imOut = None
|
||||
|
@ -323,8 +362,8 @@ def getOpenProfile(profileFilename):
|
|||
If profileFilename is not a vaild filename for an ICC profile, a PyCMSError
|
||||
will be raised.
|
||||
|
||||
:param profileFilename: String, as a valid filename path to the ICC profile you
|
||||
wish to open, or a file-like object.
|
||||
:param profileFilename: String, as a valid filename path to the ICC profile
|
||||
you wish to open, or a file-like object.
|
||||
:returns: A CmsProfile class object.
|
||||
:exception PyCMSError:
|
||||
"""
|
||||
|
@ -334,7 +373,10 @@ def getOpenProfile(profileFilename):
|
|||
except (IOError, TypeError, ValueError) as v:
|
||||
raise PyCMSError(v)
|
||||
|
||||
def buildTransform(inputProfile, outputProfile, inMode, outMode, renderingIntent=INTENT_PERCEPTUAL, flags=0):
|
||||
|
||||
def buildTransform(
|
||||
inputProfile, outputProfile, inMode, outMode,
|
||||
renderingIntent=INTENT_PERCEPTUAL, flags=0):
|
||||
"""
|
||||
(pyCMS) Builds an ICC transform mapping from the inputProfile to the
|
||||
outputProfile. Use applyTransform to apply the transform to a given
|
||||
|
@ -367,14 +409,14 @@ def buildTransform(inputProfile, outputProfile, inMode, outMode, renderingIntent
|
|||
manually overridden if you really want to, but I don't know of any
|
||||
time that would be of use, or would even work).
|
||||
|
||||
:param inputProfile: String, as a valid filename path to the ICC input profile
|
||||
you wish to use for this transform, or a profile object
|
||||
:param inputProfile: String, as a valid filename path to the ICC input
|
||||
profile you wish to use for this transform, or a profile object
|
||||
:param outputProfile: String, as a valid filename path to the ICC output
|
||||
profile you wish to use for this transform, or a profile object
|
||||
:param inMode: String, as a valid PIL mode that the appropriate profile also
|
||||
supports (i.e. "RGB", "RGBA", "CMYK", etc.)
|
||||
:param outMode: String, as a valid PIL mode that the appropriate profile also
|
||||
supports (i.e. "RGB", "RGBA", "CMYK", etc.)
|
||||
:param inMode: String, as a valid PIL mode that the appropriate profile
|
||||
also supports (i.e. "RGB", "RGBA", "CMYK", etc.)
|
||||
:param outMode: String, as a valid PIL mode that the appropriate profile
|
||||
also supports (i.e. "RGB", "RGBA", "CMYK", etc.)
|
||||
:param renderingIntent: Integer (0-3) specifying the rendering intent you
|
||||
wish to use for the transform
|
||||
|
||||
|
@ -383,28 +425,37 @@ def buildTransform(inputProfile, outputProfile, inMode, outMode, renderingIntent
|
|||
INTENT_SATURATION = 2 (ImageCms.INTENT_SATURATION)
|
||||
INTENT_ABSOLUTE_COLORIMETRIC = 3 (ImageCms.INTENT_ABSOLUTE_COLORIMETRIC)
|
||||
|
||||
see the pyCMS documentation for details on rendering intents and what they do.
|
||||
see the pyCMS documentation for details on rendering intents and what
|
||||
they do.
|
||||
:param flags: Integer (0-...) specifying additional flags
|
||||
:returns: A CmsTransform class object.
|
||||
:exception PyCMSError:
|
||||
"""
|
||||
|
||||
if not isinstance(renderingIntent, int) or not (0 <= renderingIntent <=3):
|
||||
if not isinstance(renderingIntent, int) or not (0 <= renderingIntent <= 3):
|
||||
raise PyCMSError("renderingIntent must be an integer between 0 and 3")
|
||||
|
||||
if not isinstance(flags, int) or not (0 <= flags <= _MAX_FLAG):
|
||||
raise PyCMSError("flags must be an integer between 0 and %s" + _MAX_FLAG)
|
||||
raise PyCMSError(
|
||||
"flags must be an integer between 0 and %s" + _MAX_FLAG)
|
||||
|
||||
try:
|
||||
if not isinstance(inputProfile, ImageCmsProfile):
|
||||
inputProfile = ImageCmsProfile(inputProfile)
|
||||
if not isinstance(outputProfile, ImageCmsProfile):
|
||||
outputProfile = ImageCmsProfile(outputProfile)
|
||||
return ImageCmsTransform(inputProfile, outputProfile, inMode, outMode, renderingIntent, flags=flags)
|
||||
return ImageCmsTransform(
|
||||
inputProfile, outputProfile, inMode, outMode,
|
||||
renderingIntent, flags=flags)
|
||||
except (IOError, TypeError, ValueError) as v:
|
||||
raise PyCMSError(v)
|
||||
|
||||
def buildProofTransform(inputProfile, outputProfile, proofProfile, inMode, outMode, renderingIntent=INTENT_PERCEPTUAL, proofRenderingIntent=INTENT_ABSOLUTE_COLORIMETRIC, flags=FLAGS["SOFTPROOFING"]):
|
||||
|
||||
def buildProofTransform(
|
||||
inputProfile, outputProfile, proofProfile, inMode, outMode,
|
||||
renderingIntent=INTENT_PERCEPTUAL,
|
||||
proofRenderingIntent=INTENT_ABSOLUTE_COLORIMETRIC,
|
||||
flags=FLAGS["SOFTPROOFING"]):
|
||||
"""
|
||||
(pyCMS) Builds an ICC transform mapping from the inputProfile to the
|
||||
outputProfile, but tries to simulate the result that would be
|
||||
|
@ -443,17 +494,17 @@ def buildProofTransform(inputProfile, outputProfile, proofProfile, inMode, outMo
|
|||
when the simulated device has a much wider gamut than the output
|
||||
device, you may obtain marginal results.
|
||||
|
||||
:param inputProfile: String, as a valid filename path to the ICC input profile
|
||||
you wish to use for this transform, or a profile object
|
||||
:param inputProfile: String, as a valid filename path to the ICC input
|
||||
profile you wish to use for this transform, or a profile object
|
||||
:param outputProfile: String, as a valid filename path to the ICC output
|
||||
(monitor, usually) profile you wish to use for this transform, or a
|
||||
profile object
|
||||
:param proofProfile: String, as a valid filename path to the ICC proof profile
|
||||
you wish to use for this transform, or a profile object
|
||||
:param inMode: String, as a valid PIL mode that the appropriate profile also
|
||||
supports (i.e. "RGB", "RGBA", "CMYK", etc.)
|
||||
:param outMode: String, as a valid PIL mode that the appropriate profile also
|
||||
supports (i.e. "RGB", "RGBA", "CMYK", etc.)
|
||||
:param proofProfile: String, as a valid filename path to the ICC proof
|
||||
profile you wish to use for this transform, or a profile object
|
||||
:param inMode: String, as a valid PIL mode that the appropriate profile
|
||||
also supports (i.e. "RGB", "RGBA", "CMYK", etc.)
|
||||
:param outMode: String, as a valid PIL mode that the appropriate profile
|
||||
also supports (i.e. "RGB", "RGBA", "CMYK", etc.)
|
||||
:param renderingIntent: Integer (0-3) specifying the rendering intent you
|
||||
wish to use for the input->proof (simulated) transform
|
||||
|
||||
|
@ -462,7 +513,8 @@ def buildProofTransform(inputProfile, outputProfile, proofProfile, inMode, outMo
|
|||
INTENT_SATURATION = 2 (ImageCms.INTENT_SATURATION)
|
||||
INTENT_ABSOLUTE_COLORIMETRIC = 3 (ImageCms.INTENT_ABSOLUTE_COLORIMETRIC)
|
||||
|
||||
see the pyCMS documentation for details on rendering intents and what they do.
|
||||
see the pyCMS documentation for details on rendering intents and what
|
||||
they do.
|
||||
:param proofRenderingIntent: Integer (0-3) specifying the rendering intent you
|
||||
wish to use for proof->output transform
|
||||
|
||||
|
@ -471,17 +523,19 @@ def buildProofTransform(inputProfile, outputProfile, proofProfile, inMode, outMo
|
|||
INTENT_SATURATION = 2 (ImageCms.INTENT_SATURATION)
|
||||
INTENT_ABSOLUTE_COLORIMETRIC = 3 (ImageCms.INTENT_ABSOLUTE_COLORIMETRIC)
|
||||
|
||||
see the pyCMS documentation for details on rendering intents and what they do.
|
||||
see the pyCMS documentation for details on rendering intents and what
|
||||
they do.
|
||||
:param flags: Integer (0-...) specifying additional flags
|
||||
:returns: A CmsTransform class object.
|
||||
:exception PyCMSError:
|
||||
"""
|
||||
|
||||
if not isinstance(renderingIntent, int) or not (0 <= renderingIntent <=3):
|
||||
|
||||
if not isinstance(renderingIntent, int) or not (0 <= renderingIntent <= 3):
|
||||
raise PyCMSError("renderingIntent must be an integer between 0 and 3")
|
||||
|
||||
if not isinstance(flags, int) or not (0 <= flags <= _MAX_FLAG):
|
||||
raise PyCMSError("flags must be an integer between 0 and %s" + _MAX_FLAG)
|
||||
raise PyCMSError(
|
||||
"flags must be an integer between 0 and %s" + _MAX_FLAG)
|
||||
|
||||
try:
|
||||
if not isinstance(inputProfile, ImageCmsProfile):
|
||||
|
@ -490,13 +544,16 @@ def buildProofTransform(inputProfile, outputProfile, proofProfile, inMode, outMo
|
|||
outputProfile = ImageCmsProfile(outputProfile)
|
||||
if not isinstance(proofProfile, ImageCmsProfile):
|
||||
proofProfile = ImageCmsProfile(proofProfile)
|
||||
return ImageCmsTransform(inputProfile, outputProfile, inMode, outMode, renderingIntent, proofProfile, proofRenderingIntent, flags)
|
||||
return ImageCmsTransform(
|
||||
inputProfile, outputProfile, inMode, outMode, renderingIntent,
|
||||
proofProfile, proofRenderingIntent, flags)
|
||||
except (IOError, TypeError, ValueError) as v:
|
||||
raise PyCMSError(v)
|
||||
|
||||
buildTransformFromOpenProfiles = buildTransform
|
||||
buildProofTransformFromOpenProfiles = buildProofTransform
|
||||
|
||||
|
||||
def applyTransform(im, transform, inPlace=0):
|
||||
"""
|
||||
(pyCMS) Applies a transform to a given image.
|
||||
|
@ -514,8 +571,8 @@ def applyTransform(im, transform, inPlace=0):
|
|||
is raised.
|
||||
|
||||
This function applies a pre-calculated transform (from
|
||||
ImageCms.buildTransform() or ImageCms.buildTransformFromOpenProfiles()) to an
|
||||
image. The transform can be used for multiple images, saving
|
||||
ImageCms.buildTransform() or ImageCms.buildTransformFromOpenProfiles())
|
||||
to an image. The transform can be used for multiple images, saving
|
||||
considerable calcuation time if doing the same conversion multiple times.
|
||||
|
||||
If you want to modify im in-place instead of receiving a new image as
|
||||
|
@ -528,10 +585,12 @@ def applyTransform(im, transform, inPlace=0):
|
|||
:param im: A PIL Image object, and im.mode must be the same as the inMode
|
||||
supported by the transform.
|
||||
:param transform: A valid CmsTransform class object
|
||||
:param inPlace: Bool (1 == True, 0 or None == False). If True, im is modified
|
||||
in place and None is returned, if False, a new Image object 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
|
||||
:param inPlace: Bool (1 == True, 0 or None == False). If True, im is
|
||||
modified in place and None is returned, if False, a new Image object
|
||||
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. The profile will be returned in the image's info['icc_profile'].
|
||||
:exception PyCMSError:
|
||||
"""
|
||||
|
||||
|
@ -546,6 +605,7 @@ def applyTransform(im, transform, inPlace=0):
|
|||
|
||||
return imOut
|
||||
|
||||
|
||||
def createProfile(colorSpace, colorTemp=-1):
|
||||
"""
|
||||
(pyCMS) Creates a profile.
|
||||
|
@ -562,36 +622,42 @@ def createProfile(colorSpace, colorTemp=-1):
|
|||
ImageCms.buildTransformFromOpenProfiles() to create a transform to apply
|
||||
to images.
|
||||
|
||||
:param colorSpace: String, the color space of the profile you wish to create.
|
||||
:param colorSpace: String, the color space of the profile you wish to
|
||||
create.
|
||||
Currently only "LAB", "XYZ", and "sRGB" are supported.
|
||||
:param colorTemp: Positive integer for the white point for the profile, in
|
||||
degrees Kelvin (i.e. 5000, 6500, 9600, etc.). The default is for D50
|
||||
illuminant if omitted (5000k). colorTemp is ONLY applied to LAB profiles,
|
||||
and is ignored for XYZ and sRGB.
|
||||
illuminant if omitted (5000k). colorTemp is ONLY applied to LAB
|
||||
profiles, and is ignored for XYZ and sRGB.
|
||||
:returns: A CmsProfile class object
|
||||
:exception PyCMSError:
|
||||
"""
|
||||
|
||||
if colorSpace not in ["LAB", "XYZ", "sRGB"]:
|
||||
raise PyCMSError("Color space not supported for on-the-fly profile creation (%s)" % colorSpace)
|
||||
raise PyCMSError(
|
||||
"Color space not supported for on-the-fly profile creation (%s)"
|
||||
% colorSpace)
|
||||
|
||||
if colorSpace == "LAB":
|
||||
try:
|
||||
colorTemp = float(colorTemp)
|
||||
except:
|
||||
raise PyCMSError("Color temperature must be numeric, \"%s\" not valid" % colorTemp)
|
||||
raise PyCMSError(
|
||||
"Color temperature must be numeric, \"%s\" not valid"
|
||||
% colorTemp)
|
||||
|
||||
try:
|
||||
return core.createProfile(colorSpace, colorTemp)
|
||||
except (TypeError, ValueError) as v:
|
||||
raise PyCMSError(v)
|
||||
|
||||
|
||||
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.
|
||||
|
||||
|
@ -600,10 +666,10 @@ def getProfileName(profile):
|
|||
profile was originally created. Sometimes this tag also contains
|
||||
additional information supplied by the creator.
|
||||
|
||||
:param profile: EITHER a valid CmsProfile object, OR a string of the filename
|
||||
of an ICC profile.
|
||||
:returns: A string containing the internal name of the profile as stored in an
|
||||
ICC tag.
|
||||
:param profile: EITHER a valid CmsProfile object, OR a string of the
|
||||
filename of an ICC profile.
|
||||
:returns: A string containing the internal name of the profile as stored
|
||||
in an ICC tag.
|
||||
:exception PyCMSError:
|
||||
"""
|
||||
|
||||
|
@ -612,14 +678,14 @@ def getProfileName(profile):
|
|||
if not isinstance(profile, ImageCmsProfile):
|
||||
profile = ImageCmsProfile(profile)
|
||||
# do it in python, not c.
|
||||
# // name was "%s - %s" (model, manufacturer) || Description ,
|
||||
# // but if the Model and Manufacturer were the same or the model
|
||||
# // name was "%s - %s" (model, manufacturer) || Description ,
|
||||
# // but if the Model and Manufacturer were the same or the model
|
||||
# // was long, Just the model, in 1.x
|
||||
model = profile.profile.product_model
|
||||
manufacturer = profile.profile.product_manufacturer
|
||||
|
||||
if not (model or manufacturer):
|
||||
return profile.profile.product_description+"\n"
|
||||
return profile.profile.product_description + "\n"
|
||||
if not manufacturer or len(model) > 30:
|
||||
return model + "\n"
|
||||
return "%s - %s\n" % (model, manufacturer)
|
||||
|
@ -627,6 +693,7 @@ def getProfileName(profile):
|
|||
except (AttributeError, IOError, TypeError, ValueError) as v:
|
||||
raise PyCMSError(v)
|
||||
|
||||
|
||||
def getProfileInfo(profile):
|
||||
"""
|
||||
(pyCMS) Gets the internal product information for the given profile.
|
||||
|
@ -641,18 +708,19 @@ def getProfileInfo(profile):
|
|||
info tag. This often contains details about the profile, and how it
|
||||
was created, as supplied by the creator.
|
||||
|
||||
:param profile: EITHER a valid CmsProfile object, OR a string of the filename
|
||||
of an ICC profile.
|
||||
:returns: A string containing the internal profile information stored in an ICC
|
||||
tag.
|
||||
:param profile: EITHER a valid CmsProfile object, OR a string of the
|
||||
filename of an ICC profile.
|
||||
:returns: A string containing the internal profile information stored in
|
||||
an ICC tag.
|
||||
:exception PyCMSError:
|
||||
"""
|
||||
|
||||
|
||||
try:
|
||||
if not isinstance(profile, ImageCmsProfile):
|
||||
profile = ImageCmsProfile(profile)
|
||||
# add an extra newline to preserve pyCMS compatibility
|
||||
# Python, not C. the white point bits weren't working well, so skipping.
|
||||
# Python, not C. the white point bits weren't working well,
|
||||
# so skipping.
|
||||
# // info was description \r\n\r\n copyright \r\n\r\n K007 tag \r\n\r\n whitepoint
|
||||
description = profile.profile.product_description
|
||||
cpright = profile.profile.product_copyright
|
||||
|
@ -660,7 +728,7 @@ def getProfileInfo(profile):
|
|||
for elt in (description, cpright):
|
||||
if elt:
|
||||
arr.append(elt)
|
||||
return "\r\n\r\n".join(arr)+"\r\n\r\n"
|
||||
return "\r\n\r\n".join(arr) + "\r\n\r\n"
|
||||
|
||||
except (AttributeError, IOError, TypeError, ValueError) as v:
|
||||
raise PyCMSError(v)
|
||||
|
@ -677,12 +745,12 @@ def getProfileCopyright(profile):
|
|||
is raised
|
||||
|
||||
Use this function to obtain the information stored in the profile's
|
||||
copyright tag.
|
||||
copyright tag.
|
||||
|
||||
:param profile: EITHER a valid CmsProfile object, OR a string of the filename
|
||||
of an ICC profile.
|
||||
:returns: A string containing the internal profile information stored in an ICC
|
||||
tag.
|
||||
:param profile: EITHER a valid CmsProfile object, OR a string of the
|
||||
filename of an ICC profile.
|
||||
:returns: A string containing the internal profile information stored in
|
||||
an ICC tag.
|
||||
:exception PyCMSError:
|
||||
"""
|
||||
try:
|
||||
|
@ -693,6 +761,7 @@ def getProfileCopyright(profile):
|
|||
except (AttributeError, IOError, TypeError, ValueError) as v:
|
||||
raise PyCMSError(v)
|
||||
|
||||
|
||||
def getProfileManufacturer(profile):
|
||||
"""
|
||||
(pyCMS) Gets the manufacturer for the given profile.
|
||||
|
@ -700,16 +769,16 @@ def getProfileManufacturer(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 manufacturer tag, a PyCMSError
|
||||
is raised
|
||||
If an error occurs while trying to obtain the manufacturer tag, a
|
||||
PyCMSError is raised
|
||||
|
||||
Use this function to obtain the information stored in the profile's
|
||||
manufacturer tag.
|
||||
manufacturer tag.
|
||||
|
||||
:param profile: EITHER a valid CmsProfile object, OR a string of the filename
|
||||
of an ICC profile.
|
||||
:returns: A string containing the internal profile information stored in an ICC
|
||||
tag.
|
||||
:param profile: EITHER a valid CmsProfile object, OR a string of the
|
||||
filename of an ICC profile.
|
||||
:returns: A string containing the internal profile information stored in
|
||||
an ICC tag.
|
||||
:exception PyCMSError:
|
||||
"""
|
||||
try:
|
||||
|
@ -720,23 +789,24 @@ def getProfileManufacturer(profile):
|
|||
except (AttributeError, IOError, TypeError, ValueError) as v:
|
||||
raise PyCMSError(v)
|
||||
|
||||
|
||||
def getProfileModel(profile):
|
||||
"""
|
||||
(pyCMS) Gets the model for the given 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 model tag, a PyCMSError
|
||||
is raised
|
||||
|
||||
|
||||
Use this function to obtain the information stored in the profile's
|
||||
model tag.
|
||||
|
||||
:param profile: EITHER a valid CmsProfile object, OR a string of the filename
|
||||
of an ICC profile.
|
||||
:returns: A string containing the internal profile information stored in an ICC
|
||||
tag.
|
||||
model tag.
|
||||
|
||||
:param profile: EITHER a valid CmsProfile object, OR a string of the
|
||||
filename of an ICC profile.
|
||||
:returns: A string containing the internal profile information stored in
|
||||
an ICC tag.
|
||||
:exception PyCMSError:
|
||||
"""
|
||||
|
||||
|
@ -748,6 +818,7 @@ def getProfileModel(profile):
|
|||
except (AttributeError, IOError, TypeError, ValueError) as v:
|
||||
raise PyCMSError(v)
|
||||
|
||||
|
||||
def getProfileDescription(profile):
|
||||
"""
|
||||
(pyCMS) Gets the description for the given profile.
|
||||
|
@ -759,12 +830,12 @@ def getProfileDescription(profile):
|
|||
is raised
|
||||
|
||||
Use this function to obtain the information stored in the profile's
|
||||
description tag.
|
||||
description tag.
|
||||
|
||||
:param profile: EITHER a valid CmsProfile object, OR a string of the filename
|
||||
of an ICC profile.
|
||||
:returns: A string containing the internal profile information stored in an ICC
|
||||
tag.
|
||||
:param profile: EITHER a valid CmsProfile object, OR a string of the
|
||||
filename of an ICC profile.
|
||||
:returns: A string containing the internal profile information stored in an
|
||||
ICC tag.
|
||||
:exception PyCMSError:
|
||||
"""
|
||||
|
||||
|
@ -793,16 +864,18 @@ def getDefaultIntent(profile):
|
|||
If you wish to use a different intent than returned, use
|
||||
ImageCms.isIntentSupported() to verify it will work first.
|
||||
|
||||
:param profile: EITHER a valid CmsProfile object, OR a string of the filename
|
||||
of an ICC profile.
|
||||
:returns: Integer 0-3 specifying the default rendering intent for this profile.
|
||||
:param profile: EITHER a valid CmsProfile object, OR a string of the
|
||||
filename of an ICC profile.
|
||||
:returns: Integer 0-3 specifying the default rendering intent for this
|
||||
profile.
|
||||
|
||||
INTENT_PERCEPTUAL = 0 (DEFAULT) (ImageCms.INTENT_PERCEPTUAL)
|
||||
INTENT_RELATIVE_COLORIMETRIC = 1 (ImageCms.INTENT_RELATIVE_COLORIMETRIC)
|
||||
INTENT_SATURATION = 2 (ImageCms.INTENT_SATURATION)
|
||||
INTENT_ABSOLUTE_COLORIMETRIC = 3 (ImageCms.INTENT_ABSOLUTE_COLORIMETRIC)
|
||||
|
||||
see the pyCMS documentation for details on rendering intents and what they do.
|
||||
see the pyCMS documentation for details on rendering intents and what
|
||||
they do.
|
||||
:exception PyCMSError:
|
||||
"""
|
||||
|
||||
|
@ -813,6 +886,7 @@ def getDefaultIntent(profile):
|
|||
except (AttributeError, IOError, TypeError, ValueError) as v:
|
||||
raise PyCMSError(v)
|
||||
|
||||
|
||||
def isIntentSupported(profile, intent, direction):
|
||||
"""
|
||||
(pyCMS) Checks if a given intent is supported.
|
||||
|
@ -822,23 +896,24 @@ 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
|
||||
you select.
|
||||
|
||||
:param profile: EITHER a valid CmsProfile object, OR a string of the filename
|
||||
of an ICC profile.
|
||||
:param intent: Integer (0-3) specifying the rendering intent you wish to use
|
||||
with this profile
|
||||
:param profile: EITHER a valid CmsProfile object, OR a string of the
|
||||
filename of an ICC profile.
|
||||
:param intent: Integer (0-3) specifying the rendering intent you wish to
|
||||
use with this profile
|
||||
|
||||
INTENT_PERCEPTUAL = 0 (DEFAULT) (ImageCms.INTENT_PERCEPTUAL)
|
||||
INTENT_RELATIVE_COLORIMETRIC = 1 (ImageCms.INTENT_RELATIVE_COLORIMETRIC)
|
||||
INTENT_SATURATION = 2 (ImageCms.INTENT_SATURATION)
|
||||
INTENT_ABSOLUTE_COLORIMETRIC = 3 (ImageCms.INTENT_ABSOLUTE_COLORIMETRIC)
|
||||
|
||||
see the pyCMS documentation for details on rendering intents and what they do.
|
||||
see the pyCMS documentation for details on rendering intents and what
|
||||
they do.
|
||||
:param direction: Integer specifing if the profile is to be used for input,
|
||||
output, or proof
|
||||
|
||||
|
@ -862,15 +937,17 @@ def isIntentSupported(profile, intent, direction):
|
|||
except (AttributeError, IOError, TypeError, ValueError) as v:
|
||||
raise PyCMSError(v)
|
||||
|
||||
|
||||
def versions():
|
||||
"""
|
||||
(pyCMS) Fetches versions.
|
||||
"""
|
||||
|
||||
|
||||
import sys
|
||||
return (
|
||||
VERSION, core.littlecms_version, sys.version.split()[0], Image.VERSION
|
||||
)
|
||||
VERSION, core.littlecms_version,
|
||||
sys.version.split()[0], Image.VERSION
|
||||
)
|
||||
|
||||
# --------------------------------------------------------------------
|
||||
|
||||
|
@ -880,14 +957,16 @@ if __name__ == "__main__":
|
|||
from PIL import ImageCms
|
||||
print(__doc__)
|
||||
|
||||
for f in dir(pyCMS):
|
||||
print("="*80)
|
||||
print("%s" %f)
|
||||
|
||||
for f in dir(ImageCms):
|
||||
doc = None
|
||||
try:
|
||||
exec ("doc = ImageCms.%s.__doc__" %(f))
|
||||
exec("doc = %s.__doc__" % (f))
|
||||
if "pyCMS" in doc:
|
||||
# so we don't get the __doc__ string for imported modules
|
||||
print("=" * 80)
|
||||
print("%s" % f)
|
||||
print(doc)
|
||||
except AttributeError:
|
||||
except (AttributeError, TypeError):
|
||||
pass
|
||||
|
||||
# End of file
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
#
|
||||
# For a background, see "Image Processing By Interpolation and
|
||||
# Extrapolation", Paul Haeberli and Douglas Voorhies. Available
|
||||
# at http://www.sgi.com/grafica/interp/index.html
|
||||
# at http://www.graficaobscura.com/interp/index.html
|
||||
#
|
||||
# History:
|
||||
# 1996-03-23 fl Created
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
@ -502,5 +506,5 @@ def _safe_read(fp, size):
|
|||
if not block:
|
||||
break
|
||||
data.append(block)
|
||||
size = size - len(block)
|
||||
size -= len(block)
|
||||
return b"".join(data)
|
||||
|
|
|
@ -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 = "../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
|
||||
|
|
|
@ -17,7 +17,6 @@
|
|||
|
||||
from PIL import Image
|
||||
from PIL import _imagingmath
|
||||
import sys
|
||||
|
||||
try:
|
||||
import builtins
|
||||
|
@ -27,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
|
||||
|
||||
|
@ -69,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:
|
||||
|
@ -102,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)
|
||||
|
||||
|
@ -143,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))
|
||||
|
||||
|
|
245
PIL/ImageMorph.py
Normal file
|
@ -0,0 +1,245 @@
|
|||
# A binary morphology add-on for the Python Imaging Library
|
||||
#
|
||||
# History:
|
||||
# 2014-06-04 Initial version.
|
||||
#
|
||||
# Copyright (c) 2014 Dov Grobgeld <dov.grobgeld@gmail.com>
|
||||
|
||||
from PIL import Image
|
||||
from PIL import _imagingmorph
|
||||
import re
|
||||
|
||||
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::
|
||||
|
||||
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
|
||||
|
||||
The result of the operation is described after "->" string.
|
||||
|
||||
The default is to return the current pixel value, which is
|
||||
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
|
||||
|
||||
Example::
|
||||
|
||||
lb = LutBuilder(patterns = ["4:(... .1. 111)->1"])
|
||||
lut = lb.build_lut()
|
||||
|
||||
"""
|
||||
def __init__(self, patterns=None, op_name=None):
|
||||
if patterns is not None:
|
||||
self.patterns = patterns
|
||||
else:
|
||||
self.patterns = []
|
||||
self.lut = None
|
||||
if op_name is not None:
|
||||
known_patterns = {
|
||||
'corner': ['1:(... ... ...)->0',
|
||||
'4:(00. 01. ...)->1'],
|
||||
'dilation4': ['4:(... .0. .1.)->1'],
|
||||
'dilation8': ['4:(... .0. .1.)->1',
|
||||
'4:(... .0. ..1)->1'],
|
||||
'erosion4': ['4:(... .1. .0.)->0'],
|
||||
'erosion8': ['4:(... .1. .0.)->0',
|
||||
'4:(... .1. ..0)->0'],
|
||||
'edge': ['1:(... ... ...)->0',
|
||||
'4:(.0. .1. ...)->1',
|
||||
'4:(01. .1. ...)->1']
|
||||
}
|
||||
if op_name not in known_patterns:
|
||||
raise Exception('Unknown pattern '+op_name+'!')
|
||||
|
||||
self.patterns = known_patterns[op_name]
|
||||
|
||||
def add_patterns(self, patterns):
|
||||
self.patterns += patterns
|
||||
|
||||
def build_default_lut(self):
|
||||
symbols = [0, 1]
|
||||
m = 1 << 4 # pos of current pixel
|
||||
self.lut = bytearray([symbols[(i & m) > 0] for i in range(LUT_SIZE)])
|
||||
|
||||
def get_lut(self):
|
||||
return self.lut
|
||||
|
||||
def _string_permute(self, pattern, permutation):
|
||||
"""string_permute takes a pattern and a permutation and returns the
|
||||
string permuted according to the permutation list.
|
||||
"""
|
||||
assert(len(permutation) == 9)
|
||||
return ''.join([pattern[p] for p in permutation])
|
||||
|
||||
def _pattern_permute(self, basic_pattern, options, basic_result):
|
||||
"""pattern_permute takes a basic pattern and its result and clones
|
||||
the pattern according to the modifications described in the $options
|
||||
parameter. It returns a list of all cloned patterns."""
|
||||
patterns = [(basic_pattern, basic_result)]
|
||||
|
||||
# rotations
|
||||
if '4' in options:
|
||||
res = patterns[-1][1]
|
||||
for i in range(4):
|
||||
patterns.append(
|
||||
(self._string_permute(patterns[-1][0], [6, 3, 0,
|
||||
7, 4, 1,
|
||||
8, 5, 2]), res))
|
||||
# mirror
|
||||
if 'M' in options:
|
||||
n = len(patterns)
|
||||
for pattern, res in patterns[0:n]:
|
||||
patterns.append(
|
||||
(self._string_permute(pattern, [2, 1, 0,
|
||||
5, 4, 3,
|
||||
8, 7, 6]), res))
|
||||
|
||||
# negate
|
||||
if 'N' in options:
|
||||
n = len(patterns)
|
||||
for pattern, res in patterns[0:n]:
|
||||
# Swap 0 and 1
|
||||
pattern = (pattern
|
||||
.replace('0', 'Z')
|
||||
.replace('1', '0')
|
||||
.replace('Z', '1'))
|
||||
res = '%d' % (1-int(res))
|
||||
patterns.append((pattern, res))
|
||||
|
||||
return patterns
|
||||
|
||||
def build_lut(self):
|
||||
"""Compile all patterns into a morphology lut.
|
||||
|
||||
TBD :Build based on (file) morphlut:modify_lut
|
||||
"""
|
||||
self.build_default_lut()
|
||||
patterns = []
|
||||
|
||||
# Parse and create symmetries of the patterns strings
|
||||
for p in self.patterns:
|
||||
m = re.search(
|
||||
r'(\w*):?\s*\((.+?)\)\s*->\s*(\d)', p.replace('\n', ''))
|
||||
if not m:
|
||||
raise Exception('Syntax error in pattern "'+p+'"')
|
||||
options = m.group(1)
|
||||
pattern = m.group(2)
|
||||
result = int(m.group(3))
|
||||
|
||||
# Get rid of spaces
|
||||
pattern = pattern.replace(' ', '').replace('\n', '')
|
||||
|
||||
patterns += self._pattern_permute(pattern, options, result)
|
||||
|
||||
# # Debugging
|
||||
# for p,r in patterns:
|
||||
# print p,r
|
||||
# print '--'
|
||||
|
||||
# compile the patterns into regular expressions for speed
|
||||
for i in range(len(patterns)):
|
||||
p = patterns[i][0].replace('.', 'X').replace('X', '[01]')
|
||||
p = re.compile(p)
|
||||
patterns[i] = (p, patterns[i][1])
|
||||
|
||||
# Step through table and find patterns that match.
|
||||
# Note that all the patterns are searched. The last one
|
||||
# caught overrides
|
||||
for i in range(LUT_SIZE):
|
||||
# Build the bit pattern
|
||||
bitpattern = bin(i)[2:]
|
||||
bitpattern = ('0'*(9-len(bitpattern)) + bitpattern)[::-1]
|
||||
|
||||
for p, r in patterns:
|
||||
if p.match(bitpattern):
|
||||
self.lut[i] = [0, 1][r]
|
||||
|
||||
return self.lut
|
||||
|
||||
|
||||
class MorphOp:
|
||||
"""A class for binary morphological operators"""
|
||||
|
||||
def __init__(self,
|
||||
lut=None,
|
||||
op_name=None,
|
||||
patterns=None):
|
||||
"""Create a binary morphological operator"""
|
||||
self.lut = lut
|
||||
if op_name is not None:
|
||||
self.lut = LutBuilder(op_name=op_name).build_lut()
|
||||
elif patterns is not None:
|
||||
self.lut = LutBuilder(patterns=patterns).build_lut()
|
||||
|
||||
def apply(self, image):
|
||||
"""Run a single morphological operation on an image
|
||||
|
||||
Returns a tuple of the number of changed pixels and the
|
||||
morphed image"""
|
||||
if self.lut is None:
|
||||
raise Exception('No operator loaded')
|
||||
|
||||
outimage = Image.new(image.mode, image.size, None)
|
||||
count = _imagingmorph.apply(
|
||||
bytes(self.lut), image.im.id, outimage.im.id)
|
||||
return count, outimage
|
||||
|
||||
def match(self, image):
|
||||
"""Get a list of coordinates matching the morphological operation on
|
||||
an image.
|
||||
|
||||
Returns a list of tuples of (x,y) coordinates
|
||||
of all matching pixels."""
|
||||
if self.lut is None:
|
||||
raise Exception('No operator loaded')
|
||||
|
||||
return _imagingmorph.match(bytes(self.lut), image.im.id)
|
||||
|
||||
def get_on_pixels(self, image):
|
||||
"""Get a list of all turned on pixels in a binary image
|
||||
|
||||
Returns a list of tuples of (x,y) coordinates
|
||||
of all matching pixels."""
|
||||
|
||||
return _imagingmorph.get_on_pixels(image.im.id)
|
||||
|
||||
def load_lut(self, filename):
|
||||
"""Load an operator from an mrl file"""
|
||||
with open(filename, 'rb') as f:
|
||||
self.lut = bytearray(f.read())
|
||||
|
||||
if len(self.lut) != 8192:
|
||||
self.lut = None
|
||||
raise Exception('Wrong size operator file!')
|
||||
|
||||
def save_lut(self, filename):
|
||||
"""Save an operator to an mrl file"""
|
||||
if self.lut is None:
|
||||
raise Exception('No operator loaded')
|
||||
with open(filename, 'wb') as f:
|
||||
f.write(self.lut)
|
||||
|
||||
def set_lut(self, lut):
|
||||
"""Set the lut from an external source"""
|
||||
self.lut = lut
|
||||
|
||||
# End of file
|
|
@ -94,7 +94,7 @@ def autocontrast(image, cutoff=0, ignore=None):
|
|||
cut = cut - h[lo]
|
||||
h[lo] = 0
|
||||
else:
|
||||
h[lo] = h[lo] - cut
|
||||
h[lo] -= cut
|
||||
cut = 0
|
||||
if cut <= 0:
|
||||
break
|
||||
|
@ -105,7 +105,7 @@ def autocontrast(image, cutoff=0, ignore=None):
|
|||
cut = cut - h[hi]
|
||||
h[hi] = 0
|
||||
else:
|
||||
h[hi] = h[hi] - cut
|
||||
h[hi] -= cut
|
||||
cut = 0
|
||||
if cut <= 0:
|
||||
break
|
||||
|
@ -392,7 +392,7 @@ def solarize(image, threshold=128):
|
|||
"""
|
||||
Invert all pixel values above a threshold.
|
||||
|
||||
:param image: The image to posterize.
|
||||
:param image: The image to solarize.
|
||||
:param threshold: All pixels above this greyscale level are inverted.
|
||||
:return: An image.
|
||||
"""
|
||||
|
|
|
@ -17,19 +17,20 @@
|
|||
#
|
||||
|
||||
import array
|
||||
from PIL import Image, ImageColor
|
||||
import warnings
|
||||
from PIL import ImageColor
|
||||
|
||||
|
||||
class ImagePalette:
|
||||
"Color palette for palette mapped images"
|
||||
|
||||
def __init__(self, mode = "RGB", palette = None, size = 0):
|
||||
def __init__(self, mode="RGB", palette=None, size=0):
|
||||
self.mode = mode
|
||||
self.rawmode = None # if set, palette contains raw data
|
||||
self.rawmode = None # if set, palette contains raw data
|
||||
self.palette = palette or list(range(256))*len(self.mode)
|
||||
self.colors = {}
|
||||
self.dirty = None
|
||||
if ((size == 0 and len(self.mode)*256 != len(self.palette)) or
|
||||
if ((size == 0 and len(self.mode)*256 != len(self.palette)) or
|
||||
(size != 0 and size != len(self.palette))):
|
||||
raise ValueError("wrong palette size")
|
||||
|
||||
|
@ -55,7 +56,7 @@ class ImagePalette:
|
|||
return self.palette
|
||||
arr = array.array("B", self.palette)
|
||||
if hasattr(arr, 'tobytes'):
|
||||
#py3k has a tobytes, tostring is deprecated.
|
||||
# py3k has a tobytes, tostring is deprecated.
|
||||
return arr.tobytes()
|
||||
return arr.tostring()
|
||||
|
||||
|
@ -101,11 +102,15 @@ class ImagePalette:
|
|||
fp.write("# Mode: %s\n" % self.mode)
|
||||
for i in range(256):
|
||||
fp.write("%d" % i)
|
||||
for j in range(i, len(self.palette), 256):
|
||||
fp.write(" %d" % self.palette[j])
|
||||
for j in range(i*len(self.mode), (i+1)*len(self.mode)):
|
||||
try:
|
||||
fp.write(" %d" % self.palette[j])
|
||||
except IndexError:
|
||||
fp.write(" 0")
|
||||
fp.write("\n")
|
||||
fp.close()
|
||||
|
||||
|
||||
# --------------------------------------------------------------------
|
||||
# Internal
|
||||
|
||||
|
@ -116,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 = []
|
||||
|
@ -149,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
|
||||
|
@ -174,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:
|
||||
|
@ -185,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:
|
||||
|
@ -203,4 +232,4 @@ def load(filename):
|
|||
if not lut:
|
||||
raise IOError("cannot load palette")
|
||||
|
||||
return lut # data, rawmode
|
||||
return lut # data, rawmode
|
||||
|
|
|
@ -17,7 +17,7 @@ from __future__ import print_function
|
|||
from PIL import Image
|
||||
import os, sys
|
||||
|
||||
if(sys.version_info >= (3, 3)):
|
||||
if sys.version_info >= (3, 3):
|
||||
from shlex import quote
|
||||
else:
|
||||
from pipes import quote
|
||||
|
@ -160,7 +160,7 @@ else:
|
|||
# imagemagick's display command instead.
|
||||
command = executable = "xv"
|
||||
if title:
|
||||
command = command + " -name %s" % quote(title)
|
||||
command += " -name %s" % quote(title)
|
||||
return command, executable
|
||||
|
||||
if which("xv"):
|
||||
|
|
|
@ -21,7 +21,6 @@
|
|||
# See the README file for information on usage and redistribution.
|
||||
#
|
||||
|
||||
from PIL import Image
|
||||
import operator, math
|
||||
from functools import reduce
|
||||
|
||||
|
@ -81,7 +80,7 @@ class Stat:
|
|||
for i in range(0, len(self.h), 256):
|
||||
sum = 0.0
|
||||
for j in range(256):
|
||||
sum = sum + j * self.h[i+j]
|
||||
sum += j * self.h[i + j]
|
||||
v.append(sum)
|
||||
return v
|
||||
|
||||
|
@ -92,7 +91,7 @@ class Stat:
|
|||
for i in range(0, len(self.h), 256):
|
||||
sum2 = 0.0
|
||||
for j in range(256):
|
||||
sum2 = sum2 + (j ** 2) * float(self.h[i+j])
|
||||
sum2 += (j ** 2) * float(self.h[i + j])
|
||||
v.append(sum2)
|
||||
return v
|
||||
|
||||
|
|
|
@ -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 = 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):
|
||||
|
||||
|
@ -187,7 +165,7 @@ class IptcImageFile(ImageFile.ImageFile):
|
|||
if not s:
|
||||
break
|
||||
o.write(s)
|
||||
size = size - len(s)
|
||||
size -= len(s)
|
||||
o.close()
|
||||
|
||||
try:
|
||||
|
@ -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,31 +211,31 @@ 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":
|
||||
offset = offset + 4
|
||||
while app[offset:offset+4] == b"8BIM":
|
||||
offset += 4
|
||||
# resource code
|
||||
code = JpegImagePlugin.i16(app, offset)
|
||||
offset = offset + 2
|
||||
offset += 2
|
||||
# resource name (usually empty)
|
||||
name_len = i8(app[offset])
|
||||
name = app[offset+1:offset+1+name_len]
|
||||
offset = 1 + offset + name_len
|
||||
if offset & 1:
|
||||
offset = offset + 1
|
||||
offset += 1
|
||||
# resource data block
|
||||
size = JpegImagePlugin.i32(app, offset)
|
||||
offset = offset + 4
|
||||
offset += 4
|
||||
if code == 0x0404:
|
||||
# 0x0404 contains IPTC/NAA data
|
||||
data = app[offset:offset+size]
|
||||
break
|
||||
offset = offset + size
|
||||
if offset & 1:
|
||||
offset = offset + 1
|
||||
offset += 1
|
||||
except (AttributeError, KeyError):
|
||||
pass
|
||||
|
||||
|
@ -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
|
||||
|
|
|
@ -15,20 +15,21 @@
|
|||
|
||||
__version__ = "0.1"
|
||||
|
||||
from PIL import Image, ImageFile, _binary
|
||||
from PIL import Image, ImageFile
|
||||
import struct
|
||||
import os
|
||||
import io
|
||||
|
||||
|
||||
def _parse_codestream(fp):
|
||||
"""Parse the JPEG 2000 codestream to extract the size and component
|
||||
count from the SIZ marker segment, returning a PIL (size, mode) tuple."""
|
||||
|
||||
|
||||
hdr = fp.read(2)
|
||||
lsiz = struct.unpack('>H', hdr)[0]
|
||||
siz = hdr + fp.read(lsiz - 2)
|
||||
lsiz, rsiz, xsiz, ysiz, xosiz, yosiz, xtsiz, ytsiz, \
|
||||
xtosiz, ytosiz, csiz \
|
||||
xtosiz, ytosiz, csiz \
|
||||
= struct.unpack('>HHIIIIIIIIH', siz[:38])
|
||||
ssiz = [None]*csiz
|
||||
xrsiz = [None]*csiz
|
||||
|
@ -39,22 +40,26 @@ def _parse_codestream(fp):
|
|||
|
||||
size = (xsiz - xosiz, ysiz - yosiz)
|
||||
if csiz == 1:
|
||||
mode = 'L'
|
||||
if (yrsiz[0] & 0x7f) > 8:
|
||||
mode = 'I;16'
|
||||
else:
|
||||
mode = 'L'
|
||||
elif csiz == 2:
|
||||
mode = 'LA'
|
||||
elif csiz == 3:
|
||||
mode = 'RGB'
|
||||
elif csiz == 4:
|
||||
mode == 'RGBA'
|
||||
mode = 'RGBA'
|
||||
else:
|
||||
mode = None
|
||||
|
||||
|
||||
return (size, mode)
|
||||
|
||||
|
||||
def _parse_jp2_header(fp):
|
||||
"""Parse the JP2 header box to extract size, component count and
|
||||
color space information, returning a PIL (size, mode) tuple."""
|
||||
|
||||
|
||||
# Find the JP2 header box
|
||||
header = None
|
||||
while True:
|
||||
|
@ -65,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
|
||||
|
@ -76,7 +84,8 @@ def _parse_jp2_header(fp):
|
|||
|
||||
size = None
|
||||
mode = None
|
||||
|
||||
bpc = None
|
||||
|
||||
hio = io.BytesIO(header)
|
||||
while True:
|
||||
lbox, tbox = struct.unpack('>I4s', hio.read(8))
|
||||
|
@ -90,10 +99,12 @@ def _parse_jp2_header(fp):
|
|||
|
||||
if tbox == b'ihdr':
|
||||
height, width, nc, bpc, c, unkc, ipr \
|
||||
= struct.unpack('>IIHBBBB', content)
|
||||
= struct.unpack('>IIHBBBB', content)
|
||||
size = (width, height)
|
||||
if unkc:
|
||||
if nc == 1:
|
||||
if nc == 1 and (bpc & 0x7f) > 8:
|
||||
mode = 'I;16'
|
||||
elif nc == 1:
|
||||
mode = 'L'
|
||||
elif nc == 2:
|
||||
mode = 'LA'
|
||||
|
@ -107,22 +118,28 @@ def _parse_jp2_header(fp):
|
|||
if meth == 1:
|
||||
cs = struct.unpack('>I', content[3:7])[0]
|
||||
if cs == 16: # sRGB
|
||||
if nc == 3:
|
||||
if nc == 1 and (bpc & 0x7f) > 8:
|
||||
mode = 'I;16'
|
||||
elif nc == 1:
|
||||
mode = 'L'
|
||||
elif nc == 3:
|
||||
mode = 'RGB'
|
||||
elif nc == 4:
|
||||
mode = 'RGBA'
|
||||
break
|
||||
elif cs == 17: # grayscale
|
||||
if nc == 1:
|
||||
elif cs == 17: # grayscale
|
||||
if nc == 1 and (bpc & 0x7f) > 8:
|
||||
mode = 'I;16'
|
||||
elif nc == 1:
|
||||
mode = 'L'
|
||||
elif nc == 2:
|
||||
mode = 'LA'
|
||||
break
|
||||
elif cs == 18: # sYCC
|
||||
elif cs == 18: # sYCC
|
||||
if nc == 3:
|
||||
mode = 'RGB'
|
||||
elif nc == 4:
|
||||
mode == 'RGBA'
|
||||
mode = 'RGBA'
|
||||
break
|
||||
|
||||
return (size, mode)
|
||||
|
@ -130,6 +147,7 @@ def _parse_jp2_header(fp):
|
|||
##
|
||||
# Image plugin for JPEG2000 images.
|
||||
|
||||
|
||||
class Jpeg2KImageFile(ImageFile.ImageFile):
|
||||
format = "JPEG2000"
|
||||
format_description = "JPEG 2000 (ISO 15444)"
|
||||
|
@ -141,29 +159,37 @@ class Jpeg2KImageFile(ImageFile.ImageFile):
|
|||
self.size, self.mode = _parse_codestream(self.fp)
|
||||
else:
|
||||
sig = sig + self.fp.read(8)
|
||||
|
||||
|
||||
if sig == b'\x00\x00\x00\x0cjP \x0d\x0a\x87\x0a':
|
||||
self.codec = "jp2"
|
||||
self.size, self.mode = _parse_jp2_header(self.fp)
|
||||
else:
|
||||
raise SyntaxError('not a JPEG 2000 file')
|
||||
|
||||
|
||||
if self.size is None or self.mode is None:
|
||||
raise SyntaxError('unable to determine size/mode')
|
||||
|
||||
|
||||
self.reduce = 0
|
||||
self.layers = 0
|
||||
|
||||
fd = -1
|
||||
length = -1
|
||||
|
||||
if hasattr(self.fp, "fileno"):
|
||||
try:
|
||||
fd = self.fp.fileno()
|
||||
length = os.fstat(fd).st_size
|
||||
except:
|
||||
fd = -1
|
||||
try:
|
||||
fd = self.fp.fileno()
|
||||
pos = self.fp.tell()
|
||||
self.fp.seek(0, 2)
|
||||
length = self.fp.tell()
|
||||
self.fp.seek(pos, 0)
|
||||
except:
|
||||
fd = -1
|
||||
length = -1
|
||||
|
||||
self.tile = [('jpeg2k', (0, 0) + self.size, 0,
|
||||
(self.codec, self.reduce, self.layers, fd))]
|
||||
(self.codec, self.reduce, self.layers, fd, length))]
|
||||
|
||||
def load(self):
|
||||
if self.reduce:
|
||||
|
@ -175,15 +201,17 @@ class Jpeg2KImageFile(ImageFile.ImageFile):
|
|||
if self.tile:
|
||||
# Update the reduce and layers settings
|
||||
t = self.tile[0]
|
||||
t3 = (t[3][0], self.reduce, self.layers, t[3][3])
|
||||
t3 = (t[3][0], self.reduce, self.layers, t[3][3], t[3][4])
|
||||
self.tile = [(t[0], (0, 0) + self.size, t[2], t3)]
|
||||
|
||||
|
||||
ImageFile.ImageFile.load(self)
|
||||
|
||||
|
||||
|
||||
def _accept(prefix):
|
||||
return (prefix[:4] == b'\xff\x4f\xff\x51'
|
||||
or prefix[:12] == b'\x00\x00\x00\x0cjP \x0d\x0a\x87\x0a')
|
||||
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# Save support
|
||||
|
||||
|
@ -214,7 +242,7 @@ def _save(im, fp, filename):
|
|||
fd = fp.fileno()
|
||||
except:
|
||||
fd = -1
|
||||
|
||||
|
||||
im.encoderconfig = (
|
||||
offset,
|
||||
tile_offset,
|
||||
|
@ -228,10 +256,10 @@ def _save(im, fp, filename):
|
|||
progression,
|
||||
cinema_mode,
|
||||
fd
|
||||
)
|
||||
|
||||
)
|
||||
|
||||
ImageFile._save(im, fp, [('jpeg2k', (0, 0)+im.size, 0, kind)])
|
||||
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# Registry stuff
|
||||
|
||||
|
|
|
@ -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):
|
||||
|
@ -354,12 +361,13 @@ class JpegImageFile(ImageFile.ImageFile):
|
|||
|
||||
# ALTERNATIVE: handle JPEGs via the IJG command line utilities
|
||||
|
||||
import subprocess
|
||||
import tempfile
|
||||
import os
|
||||
f, path = tempfile.mkstemp()
|
||||
os.close(f)
|
||||
if os.path.exists(self.filename):
|
||||
os.system("djpeg '%s' >'%s'" % (self.filename, path))
|
||||
subprocess.check_call(["djpeg", "-outfile", path, self.filename])
|
||||
else:
|
||||
raise ValueError("Invalid Filename")
|
||||
|
||||
|
@ -379,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:
|
||||
|
@ -404,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])
|
||||
|
@ -414,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])
|
||||
|
@ -425,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
|
||||
|
||||
|
@ -498,7 +578,7 @@ def _save(im, fp, filename):
|
|||
else:
|
||||
if subsampling in presets:
|
||||
subsampling = presets[subsampling].get('subsampling', -1)
|
||||
if qtables in presets:
|
||||
if isStringType(qtables) and qtables in presets:
|
||||
qtables = presets[qtables].get('quantization')
|
||||
|
||||
if subsampling == "4:4:4":
|
||||
|
@ -561,8 +641,8 @@ def _save(im, fp, filename):
|
|||
i = 1
|
||||
for marker in markers:
|
||||
size = struct.pack(">H", 2 + ICC_OVERHEAD_LEN + len(marker))
|
||||
extra = extra + (b"\xFF\xE2" + size + b"ICC_PROFILE\0" + o8(i) + o8(len(markers)) + marker)
|
||||
i = i + 1
|
||||
extra += b"\xFF\xE2" + size + b"ICC_PROFILE\0" + o8(i) + o8(len(markers)) + marker
|
||||
i += 1
|
||||
|
||||
# get keyword arguments
|
||||
im.encoderconfig = (
|
||||
|
@ -602,17 +682,35 @@ def _save(im, fp, filename):
|
|||
def _save_cjpeg(im, fp, filename):
|
||||
# ALTERNATIVE: handle JPEGs via the IJG command line utilities.
|
||||
import os
|
||||
file = im._dump()
|
||||
os.system("cjpeg %s >%s" % (file, filename))
|
||||
import subprocess
|
||||
tempfile = im._dump()
|
||||
subprocess.check_call(["cjpeg", "-outfile", filename, tempfile])
|
||||
try:
|
||||
os.unlink(file)
|
||||
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")
|
||||
|
|
|
@ -38,13 +38,13 @@ class BitStream:
|
|||
self.bits = 0
|
||||
continue
|
||||
self.bitbuffer = (self.bitbuffer << 8) + c
|
||||
self.bits = self.bits + 8
|
||||
self.bits += 8
|
||||
return self.bitbuffer >> (self.bits - bits) & (1 << bits) - 1
|
||||
|
||||
def skip(self, bits):
|
||||
while self.bits < bits:
|
||||
self.bitbuffer = (self.bitbuffer << 8) + i8(self.fp.read(1))
|
||||
self.bits = self.bits + 8
|
||||
self.bits += 8
|
||||
self.bits = self.bits - bits
|
||||
|
||||
def read(self, bits):
|
||||
|
|
87
PIL/MpoImagePlugin.py
Normal file
|
@ -0,0 +1,87 @@
|
|||
#
|
||||
# The Python Imaging Library.
|
||||
# $Id$
|
||||
#
|
||||
# MPO file handling
|
||||
#
|
||||
# See "Multi-Picture Format" (CIPA DC-007-Translation 2009, Standard of the
|
||||
# Camera & Imaging Products Association)
|
||||
#
|
||||
# The multi-picture object combines multiple JPEG images (with a modified EXIF
|
||||
# data format) into a single file. While it can theoretically be used much like
|
||||
# a GIF animation, it is commonly used to represent 3D photographs and is (as
|
||||
# of this writing) the most commonly used format by 3D cameras.
|
||||
#
|
||||
# History:
|
||||
# 2014-03-13 Feneric Created
|
||||
#
|
||||
# See the README file for information on usage and redistribution.
|
||||
#
|
||||
|
||||
__version__ = "0.1"
|
||||
|
||||
from PIL import Image, JpegImagePlugin
|
||||
|
||||
def _accept(prefix):
|
||||
return JpegImagePlugin._accept(prefix)
|
||||
|
||||
def _save(im, fp, filename):
|
||||
# Note that we can only save the current frame at present
|
||||
return JpegImagePlugin._save(im, fp, filename)
|
||||
|
||||
##
|
||||
# Image plugin for MPO images.
|
||||
|
||||
class MpoImageFile(JpegImagePlugin.JpegImageFile):
|
||||
|
||||
format = "MPO"
|
||||
format_description = "MPO (CIPA DC-007)"
|
||||
|
||||
def _open(self):
|
||||
self.fp.seek(0) # prep the fp in order to pass the JPEG test
|
||||
JpegImagePlugin.JpegImageFile._open(self)
|
||||
self.mpinfo = self._getmp()
|
||||
self.__framecount = self.mpinfo[0xB001]
|
||||
self.__mpoffsets = [mpent['DataOffset'] + self.info['mpoffset'] \
|
||||
for mpent in self.mpinfo[0xB002]]
|
||||
self.__mpoffsets[0] = 0
|
||||
# Note that the following assertion will only be invalid if something
|
||||
# gets broken within JpegImagePlugin.
|
||||
assert self.__framecount == len(self.__mpoffsets)
|
||||
del self.info['mpoffset'] # no longer needed
|
||||
self.__fp = self.fp # FIXME: hack
|
||||
self.__fp.seek(self.__mpoffsets[0]) # get ready to read first frame
|
||||
self.__frame = 0
|
||||
self.offset = 0
|
||||
# for now we can only handle reading and individual frame extraction
|
||||
self.readonly = 1
|
||||
|
||||
def load_seek(self, pos):
|
||||
self.__fp.seek(pos)
|
||||
|
||||
def seek(self, frame):
|
||||
if frame < 0 or frame >= self.__framecount:
|
||||
raise EOFError("no more images in MPO file")
|
||||
else:
|
||||
self.fp = self.__fp
|
||||
self.offset = self.__mpoffsets[frame]
|
||||
self.tile = [
|
||||
("jpeg", (0, 0) + self.size, self.offset, (self.mode, ""))
|
||||
]
|
||||
self.__frame = frame
|
||||
|
||||
def tell(self):
|
||||
return self.__frame
|
||||
|
||||
|
||||
# -------------------------------------------------------------------q-
|
||||
# Registry stuff
|
||||
|
||||
# Note that since MPO shares a factory with JPEG, we do not need to do a
|
||||
# separate registration for it here.
|
||||
#Image.register_open("MPO", JpegImagePlugin.jpeg_factory, _accept)
|
||||
Image.register_save("MPO", _save)
|
||||
|
||||
Image.register_extension("MPO", ".mpo")
|
||||
|
||||
Image.register_mime("MPO", "image/mpo")
|
|
@ -7,7 +7,7 @@ This is an improved version of the OleFileIO module from [PIL](http://www.python
|
|||
|
||||
As far as I know, this module is now the most complete and robust Python implementation to read MS OLE2 files, portable on several operating systems. (please tell me if you know other similar Python modules)
|
||||
|
||||
OleFileIO_PL can be used as an independent module or with PIL. The goal is to have it integrated into [Pillow](http://python-imaging.github.io/), the friendly fork of PIL.
|
||||
OleFileIO_PL can be used as an independent module or with PIL. The goal is to have it integrated into [Pillow](http://python-pillow.github.io/), the friendly fork of PIL.
|
||||
|
||||
OleFileIO\_PL is mostly meant for developers. If you are looking for tools to analyze OLE files or to extract data, then please also check [python-oletools](http://www.decalage.info/python/oletools), which are built upon OleFileIO_PL.
|
||||
|
||||
|
@ -348,4 +348,4 @@ By obtaining, using, and/or copying this software and/or its associated document
|
|||
|
||||
Permission to use, copy, modify, and distribute this software and its associated documentation for any purpose and without fee is hereby granted, provided that the above copyright notice appears in all copies, and that both that copyright notice and this permission notice appear in supporting documentation, and that the name of Secret Labs AB or the author not be used in advertising or publicity pertaining to distribution of the software without specific, written prior permission.
|
||||
|
||||
SECRET LABS AB AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL SECRET LABS AB OR THE AUTHOR BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
SECRET LABS AB AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL SECRET LABS AB OR THE AUTHOR BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
|
|
197
PIL/OleFileIO.py
|
@ -1,28 +1,29 @@
|
|||
#!/usr/local/bin/python
|
||||
# -*- coding: latin-1 -*-
|
||||
"""
|
||||
OleFileIO_PL:
|
||||
Module to read Microsoft OLE2 files (also called Structured Storage or
|
||||
Microsoft Compound Document File Format), such as Microsoft Office
|
||||
documents, Image Composer and FlashPix files, Outlook messages, ...
|
||||
This version is compatible with Python 2.6+ and 3.x
|
||||
## OleFileIO_PL:
|
||||
## Module to read Microsoft OLE2 files (also called Structured Storage or
|
||||
## Microsoft Compound Document File Format), such as Microsoft Office
|
||||
## documents, Image Composer and FlashPix files, Outlook messages, ...
|
||||
## This version is compatible with Python 2.6+ and 3.x
|
||||
|
||||
version 0.30 2014-02-04 Philippe Lagadec - http://www.decalage.info
|
||||
## version 0.30 2014-02-04 Philippe Lagadec - http://www.decalage.info
|
||||
|
||||
Project website: http://www.decalage.info/python/olefileio
|
||||
## Project website: http://www.decalage.info/python/olefileio
|
||||
|
||||
Improved version of the OleFileIO module from PIL library v1.1.6
|
||||
See: http://www.pythonware.com/products/pil/index.htm
|
||||
## Improved version of the OleFileIO module from PIL library v1.1.6
|
||||
## See: http://www.pythonware.com/products/pil/index.htm
|
||||
|
||||
The Python Imaging Library (PIL) is
|
||||
Copyright (c) 1997-2005 by Secret Labs AB
|
||||
Copyright (c) 1995-2005 by Fredrik Lundh
|
||||
OleFileIO_PL changes are Copyright (c) 2005-2014 by Philippe Lagadec
|
||||
## The Python Imaging Library (PIL) is
|
||||
|
||||
See source code and LICENSE.txt for information on usage and redistribution.
|
||||
## Copyright (c) 1997-2005 by Secret Labs AB
|
||||
## Copyright (c) 1995-2005 by Fredrik Lundh
|
||||
|
||||
## OleFileIO_PL changes are Copyright (c) 2005-2014 by Philippe Lagadec
|
||||
|
||||
## See source code and LICENSE.txt for information on usage and redistribution.
|
||||
|
||||
## WARNING: THIS IS (STILL) WORK IN PROGRESS.
|
||||
|
||||
WARNING: THIS IS (STILL) WORK IN PROGRESS.
|
||||
"""
|
||||
|
||||
# Starting with OleFileIO_PL v0.30, only Python 2.6+ and 3.x is supported
|
||||
# This import enables print() as a function rather than a keyword
|
||||
|
@ -370,8 +371,9 @@ for key in list(vars().keys()):
|
|||
def isOleFile (filename):
|
||||
"""
|
||||
Test if file is an OLE container (according to its header).
|
||||
filename: file name or path (str, unicode)
|
||||
return: True if OLE, False otherwise.
|
||||
|
||||
:param filename: file name or path (str, unicode)
|
||||
:returns: True if OLE, False otherwise.
|
||||
"""
|
||||
f = open(filename, 'rb')
|
||||
header = f.read(len(MAGIC))
|
||||
|
@ -397,8 +399,8 @@ def i16(c, o = 0):
|
|||
"""
|
||||
Converts a 2-bytes (16 bits) string to an integer.
|
||||
|
||||
c: string containing bytes to convert
|
||||
o: offset of bytes to convert in string
|
||||
:param c: string containing bytes to convert
|
||||
:param o: offset of bytes to convert in string
|
||||
"""
|
||||
return i8(c[o]) | (i8(c[o+1])<<8)
|
||||
|
||||
|
@ -407,8 +409,8 @@ def i32(c, o = 0):
|
|||
"""
|
||||
Converts a 4-bytes (32 bits) string to an integer.
|
||||
|
||||
c: string containing bytes to convert
|
||||
o: offset of bytes to convert in string
|
||||
:param c: string containing bytes to convert
|
||||
:param o: offset of bytes to convert in string
|
||||
"""
|
||||
## return int(ord(c[o])+(ord(c[o+1])<<8)+(ord(c[o+2])<<16)+(ord(c[o+3])<<24))
|
||||
## # [PL]: added int() because "<<" gives long int since Python 2.4
|
||||
|
@ -419,7 +421,8 @@ def i32(c, o = 0):
|
|||
def _clsid(clsid):
|
||||
"""
|
||||
Converts a CLSID to a human-readable string.
|
||||
clsid: string of length 16.
|
||||
|
||||
:param clsid: string of length 16.
|
||||
"""
|
||||
assert len(clsid) == 16
|
||||
# if clsid is only made of null bytes, return an empty string:
|
||||
|
@ -439,8 +442,8 @@ def _unicode(s, errors='replace'):
|
|||
"""
|
||||
Map unicode string to Latin 1. (Python with Unicode support)
|
||||
|
||||
s: UTF-16LE unicode string to convert to Latin-1
|
||||
errors: 'replace', 'ignore' or 'strict'.
|
||||
:param s: UTF-16LE unicode string to convert to Latin-1
|
||||
:param errors: 'replace', 'ignore' or 'strict'.
|
||||
"""
|
||||
#TODO: test if it OleFileIO works with Unicode strings, instead of
|
||||
# converting to Latin-1.
|
||||
|
@ -650,14 +653,14 @@ class _OleStream(io.BytesIO):
|
|||
"""
|
||||
Constructor for _OleStream class.
|
||||
|
||||
fp : file object, the OLE container or the MiniFAT stream
|
||||
sect : sector index of first sector in the stream
|
||||
size : total size of the stream
|
||||
offset : offset in bytes for the first FAT or MiniFAT sector
|
||||
sectorsize: size of one sector
|
||||
fat : array/list of sector indexes (FAT or MiniFAT)
|
||||
filesize : size of OLE file (for debugging)
|
||||
return : a BytesIO instance containing the OLE stream
|
||||
:param fp : file object, the OLE container or the MiniFAT stream
|
||||
:param sect : sector index of first sector in the stream
|
||||
:param size : total size of the stream
|
||||
:param offset : offset in bytes for the first FAT or MiniFAT sector
|
||||
:param sectorsize: size of one sector
|
||||
:param fat : array/list of sector indexes (FAT or MiniFAT)
|
||||
:param filesize : size of OLE file (for debugging)
|
||||
:returns : a BytesIO instance containing the OLE stream
|
||||
"""
|
||||
debug('_OleStream.__init__:')
|
||||
debug(' sect=%d (%X), size=%d, offset=%d, sectorsize=%d, len(fat)=%d, fp=%s'
|
||||
|
@ -793,9 +796,9 @@ class _OleDirectoryEntry:
|
|||
Constructor for an _OleDirectoryEntry object.
|
||||
Parses a 128-bytes entry from the OLE Directory stream.
|
||||
|
||||
entry : string (must be 128 bytes long)
|
||||
sid : index of this directory entry in the OLE file directory
|
||||
olefile: OleFileIO containing this directory entry
|
||||
:param entry : string (must be 128 bytes long)
|
||||
:param sid : index of this directory entry in the OLE file directory
|
||||
:param olefile: OleFileIO containing this directory entry
|
||||
"""
|
||||
self.sid = sid
|
||||
# ref to olefile is stored for future use
|
||||
|
@ -989,7 +992,7 @@ class _OleDirectoryEntry:
|
|||
"""
|
||||
Return modification time of a directory entry.
|
||||
|
||||
return: None if modification time is null, a python datetime object
|
||||
:returns: None if modification time is null, a python datetime object
|
||||
otherwise (UTC timezone)
|
||||
|
||||
new in version 0.26
|
||||
|
@ -1003,7 +1006,7 @@ class _OleDirectoryEntry:
|
|||
"""
|
||||
Return creation time of a directory entry.
|
||||
|
||||
return: None if modification time is null, a python datetime object
|
||||
:returns: None if modification time is null, a python datetime object
|
||||
otherwise (UTC timezone)
|
||||
|
||||
new in version 0.26
|
||||
|
@ -1020,7 +1023,8 @@ class OleFileIO:
|
|||
OLE container object
|
||||
|
||||
This class encapsulates the interface to an OLE 2 structured
|
||||
storage file. Use the {@link listdir} and {@link openstream} methods to
|
||||
storage file. Use the :py:meth:`~PIL.OleFileIO.OleFileIO.listdir` and
|
||||
:py:meth:`~PIL.OleFileIO.OleFileIO.openstream` methods to
|
||||
access the contents of this file.
|
||||
|
||||
Object names are given as a list of strings, one for each subentry
|
||||
|
@ -1048,8 +1052,8 @@ class OleFileIO:
|
|||
"""
|
||||
Constructor for OleFileIO class.
|
||||
|
||||
filename: file to open.
|
||||
raise_defects: minimal level for defects to be raised as exceptions.
|
||||
:param filename: file to open.
|
||||
:param raise_defects: minimal level for defects to be raised as exceptions.
|
||||
(use DEFECT_FATAL for a typical application, DEFECT_INCORRECT for a
|
||||
security-oriented application, see source code for details)
|
||||
"""
|
||||
|
@ -1068,13 +1072,13 @@ class OleFileIO:
|
|||
It may raise an IOError exception according to the minimal level chosen
|
||||
for the OleFileIO object.
|
||||
|
||||
defect_level: defect level, possible values are:
|
||||
:param defect_level: defect level, possible values are:
|
||||
DEFECT_UNSURE : a case which looks weird, but not sure it's a defect
|
||||
DEFECT_POTENTIAL : a potential defect
|
||||
DEFECT_INCORRECT : an error according to specifications, but parsing can go on
|
||||
DEFECT_FATAL : an error which cannot be ignored, parsing is impossible
|
||||
message: string describing the defect, used with raised exception.
|
||||
exception_type: exception class to be raised, IOError by default
|
||||
:param message: string describing the defect, used with raised exception.
|
||||
:param exception_type: exception class to be raised, IOError by default
|
||||
"""
|
||||
# added by [PL]
|
||||
if defect_level >= self._raise_defects_level:
|
||||
|
@ -1089,7 +1093,7 @@ class OleFileIO:
|
|||
Open an OLE2 file.
|
||||
Reads the header, FAT and directory.
|
||||
|
||||
filename: string-like or file-like object
|
||||
:param filename: string-like or file-like object
|
||||
"""
|
||||
#[PL] check if filename is a string-like or file-like object:
|
||||
# (it is better to check for a read() method)
|
||||
|
@ -1276,8 +1280,8 @@ class OleFileIO:
|
|||
Checks if a stream has not been already referenced elsewhere.
|
||||
This method should only be called once for each known stream, and only
|
||||
if stream size is not null.
|
||||
first_sect: index of first sector of the stream in FAT
|
||||
minifat: if True, stream is located in the MiniFAT, else in the FAT
|
||||
:param first_sect: index of first sector of the stream in FAT
|
||||
:param minifat: if True, stream is located in the MiniFAT, else in the FAT
|
||||
"""
|
||||
if minifat:
|
||||
debug('_check_duplicate_stream: sect=%d in MiniFAT' % first_sect)
|
||||
|
@ -1371,8 +1375,9 @@ class OleFileIO:
|
|||
def loadfat_sect(self, sect):
|
||||
"""
|
||||
Adds the indexes of the given sector to the FAT
|
||||
sect: string containing the first FAT sector, or array of long integers
|
||||
return: index of last FAT sector.
|
||||
|
||||
:param sect: string containing the first FAT sector, or array of long integers
|
||||
:returns: index of last FAT sector.
|
||||
"""
|
||||
# a FAT sector is an array of ulong integers.
|
||||
if isinstance(sect, array.array):
|
||||
|
@ -1505,8 +1510,9 @@ class OleFileIO:
|
|||
def getsect(self, sect):
|
||||
"""
|
||||
Read given sector from file on disk.
|
||||
sect: sector index
|
||||
returns a string containing the sector data.
|
||||
|
||||
:param sect: sector index
|
||||
:returns: a string containing the sector data.
|
||||
"""
|
||||
# [PL] this original code was wrong when sectors are 4KB instead of
|
||||
# 512 bytes:
|
||||
|
@ -1530,7 +1536,8 @@ class OleFileIO:
|
|||
def loaddirectory(self, sect):
|
||||
"""
|
||||
Load the directory.
|
||||
sect: sector index of directory stream.
|
||||
|
||||
:param sect: sector index of directory stream.
|
||||
"""
|
||||
# The directory is stored in a standard
|
||||
# substream, independent of its size.
|
||||
|
@ -1567,9 +1574,10 @@ class OleFileIO:
|
|||
Load a directory entry from the directory.
|
||||
This method should only be called once for each storage/stream when
|
||||
loading the directory.
|
||||
sid: index of storage/stream in the directory.
|
||||
return: a _OleDirectoryEntry object
|
||||
raise: IOError if the entry has always been referenced.
|
||||
|
||||
:param sid: index of storage/stream in the directory.
|
||||
:returns: a _OleDirectoryEntry object
|
||||
:exception IOError: if the entry has always been referenced.
|
||||
"""
|
||||
# check if SID is OK:
|
||||
if sid<0 or sid>=len(self.direntries):
|
||||
|
@ -1598,9 +1606,9 @@ class OleFileIO:
|
|||
Open a stream, either in FAT or MiniFAT according to its size.
|
||||
(openstream helper)
|
||||
|
||||
start: index of first sector
|
||||
size: size of stream (or nothing if size is unknown)
|
||||
force_FAT: if False (default), stream will be opened in FAT or MiniFAT
|
||||
:param start: index of first sector
|
||||
:param size: size of stream (or nothing if size is unknown)
|
||||
:param force_FAT: if False (default), stream will be opened in FAT or MiniFAT
|
||||
according to size. If True, it will always be opened in FAT.
|
||||
"""
|
||||
debug('OleFileIO.open(): sect=%d, size=%d, force_FAT=%s' %
|
||||
|
@ -1630,11 +1638,11 @@ class OleFileIO:
|
|||
def _list(self, files, prefix, node, streams=True, storages=False):
|
||||
"""
|
||||
(listdir helper)
|
||||
files: list of files to fill in
|
||||
prefix: current location in storage tree (list of names)
|
||||
node: current node (_OleDirectoryEntry object)
|
||||
streams: bool, include streams if True (True by default) - new in v0.26
|
||||
storages: bool, include storages if True (False by default) - new in v0.26
|
||||
:param files: list of files to fill in
|
||||
:param prefix: current location in storage tree (list of names)
|
||||
:param node: current node (_OleDirectoryEntry object)
|
||||
:param streams: bool, include streams if True (True by default) - new in v0.26
|
||||
:param storages: bool, include storages if True (False by default) - new in v0.26
|
||||
(note: the root storage is never included)
|
||||
"""
|
||||
prefix = prefix + [node.name]
|
||||
|
@ -1657,9 +1665,9 @@ class OleFileIO:
|
|||
"""
|
||||
Return a list of streams stored in this file
|
||||
|
||||
streams: bool, include streams if True (True by default) - new in v0.26
|
||||
storages: bool, include storages if True (False by default) - new in v0.26
|
||||
(note: the root storage is never included)
|
||||
:param streams: bool, include streams if True (True by default) - new in v0.26
|
||||
:param storages: bool, include storages if True (False by default) - new in v0.26
|
||||
(note: the root storage is never included)
|
||||
"""
|
||||
files = []
|
||||
self._list(files, [], self.root, streams, storages)
|
||||
|
@ -1671,12 +1679,13 @@ class OleFileIO:
|
|||
Returns directory entry of given filename. (openstream helper)
|
||||
Note: this method is case-insensitive.
|
||||
|
||||
filename: path of stream in storage tree (except root entry), either:
|
||||
:param filename: path of stream in storage tree (except root entry), either:
|
||||
|
||||
- a string using Unix path syntax, for example:
|
||||
'storage_1/storage_1.2/stream'
|
||||
- a list of storage filenames, path to the desired stream/storage.
|
||||
Example: ['storage_1', 'storage_1.2', 'stream']
|
||||
return: sid of requested filename
|
||||
:returns: sid of requested filename
|
||||
raise IOError if file not found
|
||||
"""
|
||||
|
||||
|
@ -1700,13 +1709,15 @@ class OleFileIO:
|
|||
"""
|
||||
Open a stream as a read-only file object (BytesIO).
|
||||
|
||||
filename: path of stream in storage tree (except root entry), either:
|
||||
:param filename: path of stream in storage tree (except root entry), either:
|
||||
|
||||
- a string using Unix path syntax, for example:
|
||||
'storage_1/storage_1.2/stream'
|
||||
- a list of storage filenames, path to the desired stream/storage.
|
||||
Example: ['storage_1', 'storage_1.2', 'stream']
|
||||
return: file object (read-only)
|
||||
raise IOError if filename not found, or if this is not a stream.
|
||||
|
||||
:returns: file object (read-only)
|
||||
:exception IOError: if filename not found, or if this is not a stream.
|
||||
"""
|
||||
sid = self._find(filename)
|
||||
entry = self.direntries[sid]
|
||||
|
@ -1720,8 +1731,9 @@ class OleFileIO:
|
|||
Test if given filename exists as a stream or a storage in the OLE
|
||||
container, and return its type.
|
||||
|
||||
filename: path of stream in storage tree. (see openstream for syntax)
|
||||
return: False if object does not exist, its entry type (>0) otherwise:
|
||||
:param filename: path of stream in storage tree. (see openstream for syntax)
|
||||
:returns: False if object does not exist, its entry type (>0) otherwise:
|
||||
|
||||
- STGTY_STREAM: a stream
|
||||
- STGTY_STORAGE: a storage
|
||||
- STGTY_ROOT: the root entry
|
||||
|
@ -1738,10 +1750,10 @@ class OleFileIO:
|
|||
"""
|
||||
Return modification time of a stream/storage.
|
||||
|
||||
filename: path of stream/storage in storage tree. (see openstream for
|
||||
syntax)
|
||||
return: None if modification time is null, a python datetime object
|
||||
otherwise (UTC timezone)
|
||||
:param filename: path of stream/storage in storage tree. (see openstream for
|
||||
syntax)
|
||||
:returns: None if modification time is null, a python datetime object
|
||||
otherwise (UTC timezone)
|
||||
|
||||
new in version 0.26
|
||||
"""
|
||||
|
@ -1754,10 +1766,10 @@ class OleFileIO:
|
|||
"""
|
||||
Return creation time of a stream/storage.
|
||||
|
||||
filename: path of stream/storage in storage tree. (see openstream for
|
||||
syntax)
|
||||
return: None if creation time is null, a python datetime object
|
||||
otherwise (UTC timezone)
|
||||
:param filename: path of stream/storage in storage tree. (see openstream for
|
||||
syntax)
|
||||
:returns: None if creation time is null, a python datetime object
|
||||
otherwise (UTC timezone)
|
||||
|
||||
new in version 0.26
|
||||
"""
|
||||
|
@ -1771,8 +1783,8 @@ class OleFileIO:
|
|||
Test if given filename exists as a stream or a storage in the OLE
|
||||
container.
|
||||
|
||||
filename: path of stream in storage tree. (see openstream for syntax)
|
||||
return: True if object exist, else False.
|
||||
:param filename: path of stream in storage tree. (see openstream for syntax)
|
||||
:returns: True if object exist, else False.
|
||||
"""
|
||||
try:
|
||||
sid = self._find(filename)
|
||||
|
@ -1785,9 +1797,10 @@ class OleFileIO:
|
|||
"""
|
||||
Return size of a stream in the OLE container, in bytes.
|
||||
|
||||
filename: path of stream in storage tree (see openstream for syntax)
|
||||
return: size in bytes (long integer)
|
||||
raise: IOError if file not found, TypeError if this is not a stream.
|
||||
:param filename: path of stream in storage tree (see openstream for syntax)
|
||||
:returns: size in bytes (long integer)
|
||||
:exception IOError: if file not found
|
||||
:exception TypeError: if this is not a stream
|
||||
"""
|
||||
sid = self._find(filename)
|
||||
entry = self.direntries[sid]
|
||||
|
@ -1809,11 +1822,11 @@ class OleFileIO:
|
|||
"""
|
||||
Return properties described in substream.
|
||||
|
||||
filename: path of stream in storage tree (see openstream for syntax)
|
||||
convert_time: bool, if True timestamps will be converted to Python datetime
|
||||
no_conversion: None or list of int, timestamps not to be converted
|
||||
(for example total editing time is not a real timestamp)
|
||||
return: a dictionary of values indexed by id (integer)
|
||||
:param filename: path of stream in storage tree (see openstream for syntax)
|
||||
:param convert_time: bool, if True timestamps will be converted to Python datetime
|
||||
:param no_conversion: None or list of int, timestamps not to be converted
|
||||
(for example total editing time is not a real timestamp)
|
||||
:returns: a dictionary of values indexed by id (integer)
|
||||
"""
|
||||
# make sure no_conversion is a list, just to simplify code below:
|
||||
if no_conversion == None:
|
||||
|
|
|
@ -73,9 +73,8 @@ class PSDraw:
|
|||
|
||||
def setink(self, ink):
|
||||
"""
|
||||
.. warning::
|
||||
.. warning:: This has been in the PIL API for ages but was never implemented.
|
||||
|
||||
This has been in the PIL API for ages but was never implemented.
|
||||
"""
|
||||
print("*** NOT YET IMPLEMENTED ***")
|
||||
|
||||
|
|
|
@ -12,74 +12,75 @@ __version__ = "1.0"
|
|||
from PIL import Image, ImageFile, _binary
|
||||
|
||||
_Palm8BitColormapValues = (
|
||||
( 255, 255, 255 ), ( 255, 204, 255 ), ( 255, 153, 255 ), ( 255, 102, 255 ),
|
||||
( 255, 51, 255 ), ( 255, 0, 255 ), ( 255, 255, 204 ), ( 255, 204, 204 ),
|
||||
( 255, 153, 204 ), ( 255, 102, 204 ), ( 255, 51, 204 ), ( 255, 0, 204 ),
|
||||
( 255, 255, 153 ), ( 255, 204, 153 ), ( 255, 153, 153 ), ( 255, 102, 153 ),
|
||||
( 255, 51, 153 ), ( 255, 0, 153 ), ( 204, 255, 255 ), ( 204, 204, 255 ),
|
||||
( 204, 153, 255 ), ( 204, 102, 255 ), ( 204, 51, 255 ), ( 204, 0, 255 ),
|
||||
( 204, 255, 204 ), ( 204, 204, 204 ), ( 204, 153, 204 ), ( 204, 102, 204 ),
|
||||
( 204, 51, 204 ), ( 204, 0, 204 ), ( 204, 255, 153 ), ( 204, 204, 153 ),
|
||||
( 204, 153, 153 ), ( 204, 102, 153 ), ( 204, 51, 153 ), ( 204, 0, 153 ),
|
||||
( 153, 255, 255 ), ( 153, 204, 255 ), ( 153, 153, 255 ), ( 153, 102, 255 ),
|
||||
( 153, 51, 255 ), ( 153, 0, 255 ), ( 153, 255, 204 ), ( 153, 204, 204 ),
|
||||
( 153, 153, 204 ), ( 153, 102, 204 ), ( 153, 51, 204 ), ( 153, 0, 204 ),
|
||||
( 153, 255, 153 ), ( 153, 204, 153 ), ( 153, 153, 153 ), ( 153, 102, 153 ),
|
||||
( 153, 51, 153 ), ( 153, 0, 153 ), ( 102, 255, 255 ), ( 102, 204, 255 ),
|
||||
( 102, 153, 255 ), ( 102, 102, 255 ), ( 102, 51, 255 ), ( 102, 0, 255 ),
|
||||
( 102, 255, 204 ), ( 102, 204, 204 ), ( 102, 153, 204 ), ( 102, 102, 204 ),
|
||||
( 102, 51, 204 ), ( 102, 0, 204 ), ( 102, 255, 153 ), ( 102, 204, 153 ),
|
||||
( 102, 153, 153 ), ( 102, 102, 153 ), ( 102, 51, 153 ), ( 102, 0, 153 ),
|
||||
( 51, 255, 255 ), ( 51, 204, 255 ), ( 51, 153, 255 ), ( 51, 102, 255 ),
|
||||
( 51, 51, 255 ), ( 51, 0, 255 ), ( 51, 255, 204 ), ( 51, 204, 204 ),
|
||||
( 51, 153, 204 ), ( 51, 102, 204 ), ( 51, 51, 204 ), ( 51, 0, 204 ),
|
||||
( 51, 255, 153 ), ( 51, 204, 153 ), ( 51, 153, 153 ), ( 51, 102, 153 ),
|
||||
( 51, 51, 153 ), ( 51, 0, 153 ), ( 0, 255, 255 ), ( 0, 204, 255 ),
|
||||
( 0, 153, 255 ), ( 0, 102, 255 ), ( 0, 51, 255 ), ( 0, 0, 255 ),
|
||||
( 0, 255, 204 ), ( 0, 204, 204 ), ( 0, 153, 204 ), ( 0, 102, 204 ),
|
||||
( 0, 51, 204 ), ( 0, 0, 204 ), ( 0, 255, 153 ), ( 0, 204, 153 ),
|
||||
( 0, 153, 153 ), ( 0, 102, 153 ), ( 0, 51, 153 ), ( 0, 0, 153 ),
|
||||
( 255, 255, 102 ), ( 255, 204, 102 ), ( 255, 153, 102 ), ( 255, 102, 102 ),
|
||||
( 255, 51, 102 ), ( 255, 0, 102 ), ( 255, 255, 51 ), ( 255, 204, 51 ),
|
||||
( 255, 153, 51 ), ( 255, 102, 51 ), ( 255, 51, 51 ), ( 255, 0, 51 ),
|
||||
( 255, 255, 0 ), ( 255, 204, 0 ), ( 255, 153, 0 ), ( 255, 102, 0 ),
|
||||
( 255, 51, 0 ), ( 255, 0, 0 ), ( 204, 255, 102 ), ( 204, 204, 102 ),
|
||||
( 204, 153, 102 ), ( 204, 102, 102 ), ( 204, 51, 102 ), ( 204, 0, 102 ),
|
||||
( 204, 255, 51 ), ( 204, 204, 51 ), ( 204, 153, 51 ), ( 204, 102, 51 ),
|
||||
( 204, 51, 51 ), ( 204, 0, 51 ), ( 204, 255, 0 ), ( 204, 204, 0 ),
|
||||
( 204, 153, 0 ), ( 204, 102, 0 ), ( 204, 51, 0 ), ( 204, 0, 0 ),
|
||||
( 153, 255, 102 ), ( 153, 204, 102 ), ( 153, 153, 102 ), ( 153, 102, 102 ),
|
||||
( 153, 51, 102 ), ( 153, 0, 102 ), ( 153, 255, 51 ), ( 153, 204, 51 ),
|
||||
( 153, 153, 51 ), ( 153, 102, 51 ), ( 153, 51, 51 ), ( 153, 0, 51 ),
|
||||
( 153, 255, 0 ), ( 153, 204, 0 ), ( 153, 153, 0 ), ( 153, 102, 0 ),
|
||||
( 153, 51, 0 ), ( 153, 0, 0 ), ( 102, 255, 102 ), ( 102, 204, 102 ),
|
||||
( 102, 153, 102 ), ( 102, 102, 102 ), ( 102, 51, 102 ), ( 102, 0, 102 ),
|
||||
( 102, 255, 51 ), ( 102, 204, 51 ), ( 102, 153, 51 ), ( 102, 102, 51 ),
|
||||
( 102, 51, 51 ), ( 102, 0, 51 ), ( 102, 255, 0 ), ( 102, 204, 0 ),
|
||||
( 102, 153, 0 ), ( 102, 102, 0 ), ( 102, 51, 0 ), ( 102, 0, 0 ),
|
||||
( 51, 255, 102 ), ( 51, 204, 102 ), ( 51, 153, 102 ), ( 51, 102, 102 ),
|
||||
( 51, 51, 102 ), ( 51, 0, 102 ), ( 51, 255, 51 ), ( 51, 204, 51 ),
|
||||
( 51, 153, 51 ), ( 51, 102, 51 ), ( 51, 51, 51 ), ( 51, 0, 51 ),
|
||||
( 51, 255, 0 ), ( 51, 204, 0 ), ( 51, 153, 0 ), ( 51, 102, 0 ),
|
||||
( 51, 51, 0 ), ( 51, 0, 0 ), ( 0, 255, 102 ), ( 0, 204, 102 ),
|
||||
( 0, 153, 102 ), ( 0, 102, 102 ), ( 0, 51, 102 ), ( 0, 0, 102 ),
|
||||
( 0, 255, 51 ), ( 0, 204, 51 ), ( 0, 153, 51 ), ( 0, 102, 51 ),
|
||||
( 0, 51, 51 ), ( 0, 0, 51 ), ( 0, 255, 0 ), ( 0, 204, 0 ),
|
||||
( 0, 153, 0 ), ( 0, 102, 0 ), ( 0, 51, 0 ), ( 17, 17, 17 ),
|
||||
( 34, 34, 34 ), ( 68, 68, 68 ), ( 85, 85, 85 ), ( 119, 119, 119 ),
|
||||
( 136, 136, 136 ), ( 170, 170, 170 ), ( 187, 187, 187 ), ( 221, 221, 221 ),
|
||||
( 238, 238, 238 ), ( 192, 192, 192 ), ( 128, 0, 0 ), ( 128, 0, 128 ),
|
||||
( 0, 128, 0 ), ( 0, 128, 128 ), ( 0, 0, 0 ), ( 0, 0, 0 ),
|
||||
( 0, 0, 0 ), ( 0, 0, 0 ), ( 0, 0, 0 ), ( 0, 0, 0 ),
|
||||
( 0, 0, 0 ), ( 0, 0, 0 ), ( 0, 0, 0 ), ( 0, 0, 0 ),
|
||||
( 0, 0, 0 ), ( 0, 0, 0 ), ( 0, 0, 0 ), ( 0, 0, 0 ),
|
||||
( 0, 0, 0 ), ( 0, 0, 0 ), ( 0, 0, 0 ), ( 0, 0, 0 ),
|
||||
( 0, 0, 0 ), ( 0, 0, 0 ), ( 0, 0, 0 ), ( 0, 0, 0 ),
|
||||
( 0, 0, 0 ), ( 0, 0, 0 ), ( 0, 0, 0 ), ( 0, 0, 0 ))
|
||||
(255, 255, 255), (255, 204, 255), (255, 153, 255), (255, 102, 255),
|
||||
(255, 51, 255), (255, 0, 255), (255, 255, 204), (255, 204, 204),
|
||||
(255, 153, 204), (255, 102, 204), (255, 51, 204), (255, 0, 204),
|
||||
(255, 255, 153), (255, 204, 153), (255, 153, 153), (255, 102, 153),
|
||||
(255, 51, 153), (255, 0, 153), (204, 255, 255), (204, 204, 255),
|
||||
(204, 153, 255), (204, 102, 255), (204, 51, 255), (204, 0, 255),
|
||||
(204, 255, 204), (204, 204, 204), (204, 153, 204), (204, 102, 204),
|
||||
(204, 51, 204), (204, 0, 204), (204, 255, 153), (204, 204, 153),
|
||||
(204, 153, 153), (204, 102, 153), (204, 51, 153), (204, 0, 153),
|
||||
(153, 255, 255), (153, 204, 255), (153, 153, 255), (153, 102, 255),
|
||||
(153, 51, 255), (153, 0, 255), (153, 255, 204), (153, 204, 204),
|
||||
(153, 153, 204), (153, 102, 204), (153, 51, 204), (153, 0, 204),
|
||||
(153, 255, 153), (153, 204, 153), (153, 153, 153), (153, 102, 153),
|
||||
(153, 51, 153), (153, 0, 153), (102, 255, 255), (102, 204, 255),
|
||||
(102, 153, 255), (102, 102, 255), (102, 51, 255), (102, 0, 255),
|
||||
(102, 255, 204), (102, 204, 204), (102, 153, 204), (102, 102, 204),
|
||||
(102, 51, 204), (102, 0, 204), (102, 255, 153), (102, 204, 153),
|
||||
(102, 153, 153), (102, 102, 153), (102, 51, 153), (102, 0, 153),
|
||||
( 51, 255, 255), ( 51, 204, 255), ( 51, 153, 255), ( 51, 102, 255),
|
||||
( 51, 51, 255), ( 51, 0, 255), ( 51, 255, 204), ( 51, 204, 204),
|
||||
( 51, 153, 204), ( 51, 102, 204), ( 51, 51, 204), ( 51, 0, 204),
|
||||
( 51, 255, 153), ( 51, 204, 153), ( 51, 153, 153), ( 51, 102, 153),
|
||||
( 51, 51, 153), ( 51, 0, 153), ( 0, 255, 255), ( 0, 204, 255),
|
||||
( 0, 153, 255), ( 0, 102, 255), ( 0, 51, 255), ( 0, 0, 255),
|
||||
( 0, 255, 204), ( 0, 204, 204), ( 0, 153, 204), ( 0, 102, 204),
|
||||
( 0, 51, 204), ( 0, 0, 204), ( 0, 255, 153), ( 0, 204, 153),
|
||||
( 0, 153, 153), ( 0, 102, 153), ( 0, 51, 153), ( 0, 0, 153),
|
||||
(255, 255, 102), (255, 204, 102), (255, 153, 102), (255, 102, 102),
|
||||
(255, 51, 102), (255, 0, 102), (255, 255, 51), (255, 204, 51),
|
||||
(255, 153, 51), (255, 102, 51), (255, 51, 51), (255, 0, 51),
|
||||
(255, 255, 0), (255, 204, 0), (255, 153, 0), (255, 102, 0),
|
||||
(255, 51, 0), (255, 0, 0), (204, 255, 102), (204, 204, 102),
|
||||
(204, 153, 102), (204, 102, 102), (204, 51, 102), (204, 0, 102),
|
||||
(204, 255, 51), (204, 204, 51), (204, 153, 51), (204, 102, 51),
|
||||
(204, 51, 51), (204, 0, 51), (204, 255, 0), (204, 204, 0),
|
||||
(204, 153, 0), (204, 102, 0), (204, 51, 0), (204, 0, 0),
|
||||
(153, 255, 102), (153, 204, 102), (153, 153, 102), (153, 102, 102),
|
||||
(153, 51, 102), (153, 0, 102), (153, 255, 51), (153, 204, 51),
|
||||
(153, 153, 51), (153, 102, 51), (153, 51, 51), (153, 0, 51),
|
||||
(153, 255, 0), (153, 204, 0), (153, 153, 0), (153, 102, 0),
|
||||
(153, 51, 0), (153, 0, 0), (102, 255, 102), (102, 204, 102),
|
||||
(102, 153, 102), (102, 102, 102), (102, 51, 102), (102, 0, 102),
|
||||
(102, 255, 51), (102, 204, 51), (102, 153, 51), (102, 102, 51),
|
||||
(102, 51, 51), (102, 0, 51), (102, 255, 0), (102, 204, 0),
|
||||
(102, 153, 0), (102, 102, 0), (102, 51, 0), (102, 0, 0),
|
||||
( 51, 255, 102), ( 51, 204, 102), ( 51, 153, 102), ( 51, 102, 102),
|
||||
( 51, 51, 102), ( 51, 0, 102), ( 51, 255, 51), ( 51, 204, 51),
|
||||
( 51, 153, 51), ( 51, 102, 51), ( 51, 51, 51), ( 51, 0, 51),
|
||||
( 51, 255, 0), ( 51, 204, 0), ( 51, 153, 0), ( 51, 102, 0),
|
||||
( 51, 51, 0), ( 51, 0, 0), ( 0, 255, 102), ( 0, 204, 102),
|
||||
( 0, 153, 102), ( 0, 102, 102), ( 0, 51, 102), ( 0, 0, 102),
|
||||
( 0, 255, 51), ( 0, 204, 51), ( 0, 153, 51), ( 0, 102, 51),
|
||||
( 0, 51, 51), ( 0, 0, 51), ( 0, 255, 0), ( 0, 204, 0),
|
||||
( 0, 153, 0), ( 0, 102, 0), ( 0, 51, 0), ( 17, 17, 17),
|
||||
( 34, 34, 34), ( 68, 68, 68), ( 85, 85, 85), (119, 119, 119),
|
||||
(136, 136, 136), (170, 170, 170), (187, 187, 187), (221, 221, 221),
|
||||
(238, 238, 238), (192, 192, 192), (128, 0, 0), (128, 0, 128),
|
||||
( 0, 128, 0), ( 0, 128, 128), ( 0, 0, 0), ( 0, 0, 0),
|
||||
( 0, 0, 0), ( 0, 0, 0), ( 0, 0, 0), ( 0, 0, 0),
|
||||
( 0, 0, 0), ( 0, 0, 0), ( 0, 0, 0), ( 0, 0, 0),
|
||||
( 0, 0, 0), ( 0, 0, 0), ( 0, 0, 0), ( 0, 0, 0),
|
||||
( 0, 0, 0), ( 0, 0, 0), ( 0, 0, 0), ( 0, 0, 0),
|
||||
( 0, 0, 0), ( 0, 0, 0), ( 0, 0, 0), ( 0, 0, 0),
|
||||
( 0, 0, 0), ( 0, 0, 0), ( 0, 0, 0), ( 0, 0, 0))
|
||||
|
||||
|
||||
# so build a prototype image to be used for palette resampling
|
||||
def build_prototype_image():
|
||||
image = Image.new("L", (1,len(_Palm8BitColormapValues),))
|
||||
image = Image.new("L", (1, len(_Palm8BitColormapValues),))
|
||||
image.putdata(list(range(len(_Palm8BitColormapValues))))
|
||||
palettedata = ()
|
||||
for i in range(len(_Palm8BitColormapValues)):
|
||||
|
@ -91,7 +92,8 @@ def build_prototype_image():
|
|||
|
||||
Palm8BitColormapImage = build_prototype_image()
|
||||
|
||||
# OK, we now have in Palm8BitColormapImage, a "P"-mode image with the right palette
|
||||
# OK, we now have in Palm8BitColormapImage,
|
||||
# a "P"-mode image with the right palette
|
||||
#
|
||||
# --------------------------------------------------------------------
|
||||
|
||||
|
@ -110,6 +112,7 @@ _COMPRESSION_TYPES = {
|
|||
o8 = _binary.o8
|
||||
o16b = _binary.o16be
|
||||
|
||||
|
||||
#
|
||||
# --------------------------------------------------------------------
|
||||
|
||||
|
@ -127,12 +130,16 @@ def _save(im, fp, filename, check=0):
|
|||
bpp = 8
|
||||
version = 1
|
||||
|
||||
elif im.mode == "L" and "bpp" in im.encoderinfo and im.encoderinfo["bpp"] in (1, 2, 4):
|
||||
elif (im.mode == "L" and
|
||||
"bpp" in im.encoderinfo and
|
||||
im.encoderinfo["bpp"] in (1, 2, 4)):
|
||||
|
||||
# this is 8-bit grayscale, so we shift it to get the high-order bits, and invert it because
|
||||
# this is 8-bit grayscale, so we shift it to get the high-order bits,
|
||||
# and invert it because
|
||||
# Palm does greyscale from white (0) to black (1)
|
||||
bpp = im.encoderinfo["bpp"]
|
||||
im = im.point(lambda x, shift=8-bpp, maxval=(1 << bpp)-1: maxval - (x >> shift))
|
||||
im = im.point(
|
||||
lambda x, shift=8-bpp, maxval=(1 << bpp)-1: maxval - (x >> shift))
|
||||
# we ignore the palette here
|
||||
im.mode = "P"
|
||||
rawmode = "P;" + str(bpp)
|
||||
|
@ -140,8 +147,9 @@ def _save(im, fp, filename, check=0):
|
|||
|
||||
elif im.mode == "L" and "bpp" in im.info and im.info["bpp"] in (1, 2, 4):
|
||||
|
||||
# here we assume that even though the inherent mode is 8-bit grayscale, only
|
||||
# the lower bpp bits are significant. We invert them to match the Palm.
|
||||
# here we assume that even though the inherent mode is 8-bit grayscale,
|
||||
# only the lower bpp bits are significant.
|
||||
# We invert them to match the Palm.
|
||||
bpp = im.info["bpp"]
|
||||
im = im.point(lambda x, maxval=(1 << bpp)-1: maxval - (x & maxval))
|
||||
# we ignore the palette here
|
||||
|
@ -172,21 +180,21 @@ def _save(im, fp, filename, check=0):
|
|||
cols = im.size[0]
|
||||
rows = im.size[1]
|
||||
|
||||
rowbytes = ((cols + (16//bpp - 1)) / (16 // bpp)) * 2;
|
||||
rowbytes = int((cols + (16//bpp - 1)) / (16 // bpp)) * 2
|
||||
transparent_index = 0
|
||||
compression_type = _COMPRESSION_TYPES["none"]
|
||||
|
||||
flags = 0;
|
||||
flags = 0
|
||||
if im.mode == "P" and "custom-colormap" in im.info:
|
||||
flags = flags & _FLAGS["custom-colormap"]
|
||||
colormapsize = 4 * 256 + 2;
|
||||
colormapsize = 4 * 256 + 2
|
||||
colormapmode = im.palette.mode
|
||||
colormap = im.getdata().getpalette()
|
||||
else:
|
||||
colormapsize = 0
|
||||
|
||||
if "offset" in im.info:
|
||||
offset = (rowbytes * rows + 16 + 3 + colormapsize) // 4;
|
||||
offset = (rowbytes * rows + 16 + 3 + colormapsize) // 4
|
||||
else:
|
||||
offset = 0
|
||||
|
||||
|
@ -205,12 +213,19 @@ def _save(im, fp, filename, check=0):
|
|||
for i in range(256):
|
||||
fp.write(o8(i))
|
||||
if colormapmode == 'RGB':
|
||||
fp.write(o8(colormap[3 * i]) + o8(colormap[3 * i + 1]) + o8(colormap[3 * i + 2]))
|
||||
fp.write(
|
||||
o8(colormap[3 * i]) +
|
||||
o8(colormap[3 * i + 1]) +
|
||||
o8(colormap[3 * i + 2]))
|
||||
elif colormapmode == 'RGBA':
|
||||
fp.write(o8(colormap[4 * i]) + o8(colormap[4 * i + 1]) + o8(colormap[4 * i + 2]))
|
||||
fp.write(
|
||||
o8(colormap[4 * i]) +
|
||||
o8(colormap[4 * i + 1]) +
|
||||
o8(colormap[4 * i + 2]))
|
||||
|
||||
# now convert data to raw form
|
||||
ImageFile._save(im, fp, [("raw", (0,0)+im.size, 0, (rawmode, rowbytes, 1))])
|
||||
ImageFile._save(
|
||||
im, fp, [("raw", (0, 0)+im.size, 0, (rawmode, rowbytes, 1))])
|
||||
|
||||
fp.flush()
|
||||
|
||||
|
|
|
@ -135,7 +135,7 @@ def _save(im, fp, filename, check=0):
|
|||
# bytes per plane
|
||||
stride = (im.size[0] * bits + 7) // 8
|
||||
# stride should be even
|
||||
stride = stride + (stride % 2)
|
||||
stride += stride % 2
|
||||
# Stride needs to be kept in sync with the PcxEncode.c version.
|
||||
# Ideally it should be passed in in the state, but the bytes value
|
||||
# gets overwritten.
|
||||
|
|
|
@ -108,8 +108,8 @@ def _save(im, fp, filename):
|
|||
r = i8(palette[i*3])
|
||||
g = i8(palette[i*3+1])
|
||||
b = i8(palette[i*3+2])
|
||||
colorspace = colorspace + "%02x%02x%02x " % (r, g, b)
|
||||
colorspace = colorspace + "> ]"
|
||||
colorspace += "%02x%02x%02x " % (r, g, b)
|
||||
colorspace += "> ]"
|
||||
procset = "/ImageI" # indexed color
|
||||
elif im.mode == "RGB":
|
||||
filter = "/DCTDecode"
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -235,7 +235,7 @@ def _layerinfo(file):
|
|||
if t:
|
||||
tile.extend(t)
|
||||
layers[i] = name, mode, bbox, tile
|
||||
i = i + 1
|
||||
i += 1
|
||||
|
||||
return layers
|
||||
|
||||
|
@ -258,7 +258,7 @@ def _maketile(file, mode, bbox, channels):
|
|||
for channel in range(channels):
|
||||
layer = mode[channel]
|
||||
if mode == "CMYK":
|
||||
layer = layer + ";I"
|
||||
layer += ";I"
|
||||
tile.append(("raw", bbox, offset, layer))
|
||||
offset = offset + xsize*ysize
|
||||
|
||||
|
@ -272,13 +272,13 @@ def _maketile(file, mode, bbox, channels):
|
|||
for channel in range(channels):
|
||||
layer = mode[channel]
|
||||
if mode == "CMYK":
|
||||
layer = layer + ";I"
|
||||
layer += ";I"
|
||||
tile.append(
|
||||
("packbits", bbox, offset, layer)
|
||||
)
|
||||
for y in range(ysize):
|
||||
offset = offset + i16(bytecount[i:i+2])
|
||||
i = i + 2
|
||||
i += 2
|
||||
|
||||
file.seek(offset)
|
||||
|
||||
|
|
|
@ -261,6 +261,7 @@ mode_map = {'1': _PyAccess8,
|
|||
'PA': _PyAccess32_2,
|
||||
'RGB': _PyAccess32_3,
|
||||
'LAB': _PyAccess32_3,
|
||||
'HSV': _PyAccess32_3,
|
||||
'YCbCr': _PyAccess32_3,
|
||||
'RGBA': _PyAccess32_4,
|
||||
'RGBa': _PyAccess32_4,
|
||||
|
|
|
@ -31,6 +31,7 @@ i32 = _binary.i32be
|
|||
def _accept(prefix):
|
||||
return i16(prefix) == 474
|
||||
|
||||
|
||||
##
|
||||
# Image plugin for SGI images.
|
||||
|
||||
|
@ -44,7 +45,7 @@ class SgiImageFile(ImageFile.ImageFile):
|
|||
# HEAD
|
||||
s = self.fp.read(512)
|
||||
if i16(s) != 474:
|
||||
raise SyntaxError("not an SGI image file")
|
||||
raise ValueError("Not an SGI image file")
|
||||
|
||||
# relevant header entries
|
||||
compression = i8(s[2])
|
||||
|
@ -60,22 +61,22 @@ class SgiImageFile(ImageFile.ImageFile):
|
|||
elif layout == (1, 3, 4):
|
||||
self.mode = "RGBA"
|
||||
else:
|
||||
raise SyntaxError("unsupported SGI image mode")
|
||||
raise ValueError("Unsupported SGI image mode")
|
||||
|
||||
# size
|
||||
self.size = i16(s[6:]), i16(s[8:])
|
||||
|
||||
|
||||
# decoder info
|
||||
if compression == 0:
|
||||
offset = 512
|
||||
pagesize = self.size[0]*self.size[1]*layout[0]
|
||||
self.tile = []
|
||||
for layer in self.mode:
|
||||
self.tile.append(("raw", (0,0)+self.size, offset, (layer,0,-1)))
|
||||
self.tile.append(
|
||||
("raw", (0, 0)+self.size, offset, (layer, 0, -1)))
|
||||
offset = offset + pagesize
|
||||
elif compression == 1:
|
||||
self.tile = [("sgi_rle", (0,0)+self.size, 512, (self.mode, 0, -1))]
|
||||
raise ValueError("SGI RLE encoding not supported")
|
||||
|
||||
#
|
||||
# registry
|
||||
|
@ -85,5 +86,6 @@ Image.register_open("SGI", SgiImageFile, _accept)
|
|||
Image.register_extension("SGI", ".bw")
|
||||
Image.register_extension("SGI", ".rgb")
|
||||
Image.register_extension("SGI", ".rgba")
|
||||
Image.register_extension("SGI", ".sgi")
|
||||
|
||||
Image.register_extension("SGI", ".sgi") # really?
|
||||
# End of file
|
||||
|
|
|
@ -29,6 +29,7 @@ i32 = _binary.i32be
|
|||
def _accept(prefix):
|
||||
return i32(prefix) == 0x59a66a95
|
||||
|
||||
|
||||
##
|
||||
# Image plugin for Sun raster files.
|
||||
|
||||
|
@ -70,9 +71,9 @@ class SunImageFile(ImageFile.ImageFile):
|
|||
stride = (((self.size[0] * depth + 7) // 8) + 3) & (~3)
|
||||
|
||||
if compression == 1:
|
||||
self.tile = [("raw", (0,0)+self.size, offset, (rawmode, stride))]
|
||||
self.tile = [("raw", (0, 0)+self.size, offset, (rawmode, stride))]
|
||||
elif compression == 2:
|
||||
self.tile = [("sun_rle", (0,0)+self.size, offset, rawmode)]
|
||||
self.tile = [("sun_rle", (0, 0)+self.size, offset, rawmode)]
|
||||
|
||||
#
|
||||
# registry
|
||||
|
|
|
@ -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.
|
||||
# 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.
|
||||
|
||||
|
@ -238,7 +241,7 @@ class ImageFileDirectory(collections.MutableMapping):
|
|||
Value: integer corresponding to the data type from
|
||||
`TiffTags.TYPES`
|
||||
|
||||
'internal'
|
||||
'internal'
|
||||
* self.tags = {} Key: numerical tiff tag number
|
||||
Value: Decoded data, Generally a tuple.
|
||||
* If set from __setval__ -- always a tuple
|
||||
|
@ -276,7 +279,7 @@ 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
|
||||
|
||||
def __str__(self):
|
||||
|
@ -287,7 +290,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 +300,6 @@ class ImageFileDirectory(collections.MutableMapping):
|
|||
result[tag_name] = value
|
||||
return result
|
||||
|
||||
|
||||
# dictionary API
|
||||
|
||||
def __len__(self):
|
||||
|
@ -305,7 +309,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 +323,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:
|
||||
|
@ -433,7 +437,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
|
||||
|
||||
|
@ -449,14 +453,17 @@ class ImageFileDirectory(collections.MutableMapping):
|
|||
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])
|
||||
|
@ -489,10 +496,10 @@ class ImageFileDirectory(collections.MutableMapping):
|
|||
|
||||
if tag in self.tagtype:
|
||||
typ = self.tagtype[tag]
|
||||
|
||||
|
||||
if Image.DEBUG:
|
||||
print ("Tag %s, Type: %s, Value: %s" % (tag, typ, value))
|
||||
|
||||
|
||||
if typ == 1:
|
||||
# byte data
|
||||
if isinstance(value, tuple):
|
||||
|
@ -512,14 +519,14 @@ class ImageFileDirectory(collections.MutableMapping):
|
|||
# and doesn't match the tiff spec: 8-bit byte that
|
||||
# contains a 7-bit ASCII code; the last byte must be
|
||||
# NUL (binary zero). Also, I don't think this was well
|
||||
# excersized before.
|
||||
# excersized before.
|
||||
data = value = b"" + value.encode('ascii', 'replace') + b"\0"
|
||||
else:
|
||||
# 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 +548,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:
|
||||
|
@ -558,9 +566,9 @@ class ImageFileDirectory(collections.MutableMapping):
|
|||
count = count // 2 # adjust for rational data field
|
||||
|
||||
append((tag, typ, count, o32(offset), data))
|
||||
offset = offset + len(data)
|
||||
offset += len(data)
|
||||
if offset & 1:
|
||||
offset = offset + 1 # word padding
|
||||
offset += 1 # word padding
|
||||
|
||||
# update strip offset data to point beyond auxiliary data
|
||||
if stripoffsets is not None:
|
||||
|
@ -576,7 +584,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 +594,7 @@ class ImageFileDirectory(collections.MutableMapping):
|
|||
|
||||
return offset
|
||||
|
||||
|
||||
##
|
||||
# Image plugin for TIFF files.
|
||||
|
||||
|
@ -616,7 +625,7 @@ 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):
|
||||
|
@ -644,7 +653,7 @@ class TiffImageFile(ImageFile.ImageFile):
|
|||
self.fp.seek(self.__next)
|
||||
self.tag.load(self.fp)
|
||||
self.__next = self.tag.next
|
||||
self.__frame = self.__frame + 1
|
||||
self.__frame += 1
|
||||
self._setup()
|
||||
|
||||
def _tell(self):
|
||||
|
@ -694,9 +703,11 @@ 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)
|
||||
decoder = Image._getdecoder(self.mode, 'libtiff', args,
|
||||
self.decoderconfig)
|
||||
try:
|
||||
decoder.setimage(self.im, extents)
|
||||
except ValueError:
|
||||
|
@ -706,35 +717,35 @@ 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
|
||||
self.fp = None # might be shared
|
||||
|
||||
if err < 0:
|
||||
raise IOError(err)
|
||||
|
@ -810,11 +821,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 +836,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 +864,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.
|
||||
|
@ -859,7 +874,7 @@ class TiffImageFile(ImageFile.ImageFile):
|
|||
# libtiff handles the fillmode for us, so 1;IR should
|
||||
# actually be 1;I. Including the R double reverses the
|
||||
# bits, so stripes of the image are reversed. See
|
||||
# https://github.com/python-imaging/Pillow/issues/279
|
||||
# https://github.com/python-pillow/Pillow/issues/279
|
||||
if fillorder == 2:
|
||||
key = (
|
||||
self.tag.prefix, photo, format, 1,
|
||||
|
@ -881,7 +896,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,14 +908,14 @@ 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
|
||||
if y >= self.size[1]:
|
||||
x = y = 0
|
||||
l = l + 1
|
||||
l += 1
|
||||
a = None
|
||||
elif TILEOFFSETS in self.tag:
|
||||
# tiled image
|
||||
|
@ -914,14 +929,14 @@ 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
|
||||
if y >= self.size[1]:
|
||||
x = y = 0
|
||||
l = l + 1
|
||||
l += 1
|
||||
a = None
|
||||
else:
|
||||
if Image.DEBUG:
|
||||
|
@ -937,25 +952,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 +980,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 +991,7 @@ def _cvt_res(value):
|
|||
value = float(value)
|
||||
return (int(value * 65536), 65536)
|
||||
|
||||
|
||||
def _save(im, fp, filename):
|
||||
|
||||
try:
|
||||
|
@ -982,13 +1001,14 @@ 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'
|
||||
libtiff = WRITE_LIBTIFF or compression != 'raw'
|
||||
|
||||
# required for color libtiff images
|
||||
ifd[PLANAR_CONFIGURATION] = getattr(im, '_planar_configuration', 1)
|
||||
|
||||
|
||||
# -- multi-page -- skip TIFF header on subsequent pages
|
||||
if not libtiff and fp.tell() == 0:
|
||||
# tiff header (write via IFD to get everything right)
|
||||
|
@ -999,17 +1019,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
|
||||
|
@ -1025,12 +1044,12 @@ def _save(im, fp, filename):
|
|||
# which support profiles as TIFF) -- 2008-06-06 Florian Hoech
|
||||
if "icc_profile" in im.info:
|
||||
ifd[ICCPROFILE] = im.info["icc_profile"]
|
||||
|
||||
|
||||
if "description" in im.encoderinfo:
|
||||
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 +1096,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 +1109,27 @@ def _save(im, fp, filename):
|
|||
fp.seek(0)
|
||||
_fp = os.dup(fp.fileno())
|
||||
|
||||
blocklist = [STRIPOFFSETS, STRIPBYTECOUNTS, ROWSPERSTRIP, ICCPROFILE] # ICC Profile crashes.
|
||||
atts={}
|
||||
# bits per sample is a single short in the tiff directory, not a list.
|
||||
# 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 +1139,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 +1166,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 +1181,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
|
||||
|
||||
#
|
||||
|
|
|
@ -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,7 @@ TAGS = {
|
|||
50716: "BlackLevelDeltaV",
|
||||
50717: "WhiteLevel",
|
||||
50718: "DefaultScale",
|
||||
50741: "BestQualityScale",
|
||||
50741: "BestQualityScale", # FIXME! Dictionary contains duplicate keys 50741
|
||||
50719: "DefaultCropOrigin",
|
||||
50720: "DefaultCropSize",
|
||||
50778: "CalibrationIlluminant1",
|
||||
|
@ -185,7 +279,7 @@ TAGS = {
|
|||
50737: "ChromaBlurRadius",
|
||||
50738: "AntiAliasStrength",
|
||||
50740: "DNGPrivateData",
|
||||
50741: "MakerNoteSafety",
|
||||
50741: "MakerNoteSafety", # FIXME! Dictionary contains duplicate keys 50741
|
||||
|
||||
#ImageJ
|
||||
50838: "ImageJMetaDataByteCounts", # private tag registered with Adobe
|
||||
|
|
|
@ -14,11 +14,11 @@
|
|||
#
|
||||
|
||||
# NOTE: This format cannot be automatically recognized, so the reader
|
||||
# is not registered for use with Image.open(). To open a WEL file, use
|
||||
# is not registered for use with Image.open(). To open a WAL file, use
|
||||
# the WalImageFile.open() function instead.
|
||||
|
||||
# This reader is based on the specification available from:
|
||||
# http://www.flipcode.com/tutorials/tut_q2levels.shtml
|
||||
# http://www.flipcode.com/archives/Quake_2_BSP_File_Format.shtml
|
||||
# and has been tested with a few sample files found using google.
|
||||
|
||||
from __future__ import print_function
|
||||
|
|
|
@ -59,7 +59,7 @@ word = _binary.i16le
|
|||
def short(c, o=0):
|
||||
v = word(c, o)
|
||||
if v >= 32768:
|
||||
v = v - 65536
|
||||
v -= 65536
|
||||
return v
|
||||
|
||||
dword = _binary.i32le
|
||||
|
|
|
@ -29,6 +29,7 @@ xpm_head = re.compile(b"\"([0-9]*) ([0-9]*) ([0-9]*) ([0-9]*)")
|
|||
def _accept(prefix):
|
||||
return prefix[:9] == b"/* XPM */"
|
||||
|
||||
|
||||
##
|
||||
# Image plugin for X11 pixel maps.
|
||||
|
||||
|
@ -86,9 +87,9 @@ class XpmImageFile(ImageFile.ImageFile):
|
|||
elif rgb[0:1] == b"#":
|
||||
# FIXME: handle colour names (see ImagePalette.py)
|
||||
rgb = int(rgb[1:], 16)
|
||||
palette[c] = o8((rgb >> 16) & 255) +\
|
||||
o8((rgb >> 8) & 255) +\
|
||||
o8(rgb & 255)
|
||||
palette[c] = (o8((rgb >> 16) & 255) +
|
||||
o8((rgb >> 8) & 255) +
|
||||
o8(rgb & 255))
|
||||
else:
|
||||
# unknown colour
|
||||
raise ValueError("cannot read this XPM file")
|
||||
|
|
|
@ -12,10 +12,9 @@
|
|||
# ;-)
|
||||
|
||||
VERSION = '1.1.7' # PIL version
|
||||
PILLOW_VERSION = '2.4.0' # Pillow
|
||||
PILLOW_VERSION = '2.5.3' # Pillow
|
||||
|
||||
_plugins = ['ArgImagePlugin',
|
||||
'BmpImagePlugin',
|
||||
_plugins = ['BmpImagePlugin',
|
||||
'BufrStubImagePlugin',
|
||||
'CurImagePlugin',
|
||||
'DcxImagePlugin',
|
||||
|
@ -37,6 +36,7 @@ _plugins = ['ArgImagePlugin',
|
|||
'McIdasImagePlugin',
|
||||
'MicImagePlugin',
|
||||
'MpegImagePlugin',
|
||||
'MpoImagePlugin',
|
||||
'MspImagePlugin',
|
||||
'PalmImagePlugin',
|
||||
'PcdImagePlugin',
|
||||
|
|
|
@ -3,20 +3,25 @@ import os
|
|||
if bytes is str:
|
||||
def isStringType(t):
|
||||
return isinstance(t, basestring)
|
||||
|
||||
def isPath(f):
|
||||
return isinstance(f, basestring)
|
||||
else:
|
||||
def isStringType(t):
|
||||
return isinstance(t, str)
|
||||
|
||||
def isPath(f):
|
||||
return isinstance(f, (bytes, str))
|
||||
|
||||
|
||||
# Checks if an object is a string, and that it points to a directory.
|
||||
def isDirectory(f):
|
||||
return isPath(f) and os.path.isdir(f)
|
||||
|
||||
|
||||
class deferred_error(object):
|
||||
def __init__(self, ex):
|
||||
self.ex = ex
|
||||
|
||||
def __getattr__(self, elt):
|
||||
raise self.ex
|
||||
|
|
17
PIL/tests.py
|
@ -1,17 +0,0 @@
|
|||
import unittest
|
||||
|
||||
|
||||
class PillowTests(unittest.TestCase):
|
||||
"""
|
||||
Can we start moving the test suite here?
|
||||
"""
|
||||
|
||||
def test_suite_should_move_here(self):
|
||||
"""
|
||||
Great idea!
|
||||
"""
|
||||
assert True is True
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
11
README.rst
|
@ -3,10 +3,10 @@ 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.
|
||||
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-imaging/Pillow.svg?branch=master
|
||||
:target: https://travis-ci.org/python-imaging/Pillow
|
||||
.. image:: https://travis-ci.org/python-pillow/Pillow.svg?branch=master
|
||||
:target: https://travis-ci.org/python-pillow/Pillow
|
||||
:alt: Travis CI build status
|
||||
|
||||
.. image:: https://pypip.in/v/Pillow/badge.png
|
||||
|
@ -17,7 +17,6 @@ Pillow is the "friendly" PIL fork by Alex Clark and Contributors. PIL is the Pyt
|
|||
:target: https://pypi.python.org/pypi/Pillow/
|
||||
:alt: Number of PyPI downloads
|
||||
|
||||
.. image:: https://coveralls.io/repos/python-imaging/Pillow/badge.png?branch=master
|
||||
:target: https://coveralls.io/r/python-imaging/Pillow?branch=master
|
||||
.. image:: https://coveralls.io/repos/python-pillow/Pillow/badge.png?branch=master
|
||||
:target: https://coveralls.io/r/python-pillow/Pillow?branch=master
|
||||
|
||||
The documentation is hosted at http://pillow.readthedocs.org/. It contains installation instructions, tutorials, reference, compatibility details, and more.
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
|
||||
Python SANE module V1.1 (30 Sep. 2004)
|
||||
================================================================================
|
||||
|
||||
The SANE module provides an interface to the SANE scanner and frame
|
||||
grabber interface for Linux. This module was contributed by Andrew
|
||||
|
@ -9,11 +9,11 @@ word 'SANE' or 'sane' in the subject of your mail, otherwise it might
|
|||
be classified as spam in the future.
|
||||
|
||||
|
||||
To build this module, type (in the Sane directory):
|
||||
To build this module, type (in the Sane directory)::
|
||||
|
||||
python setup.py build
|
||||
|
||||
In order to install the module type:
|
||||
In order to install the module type::
|
||||
|
||||
python setup.py install
|
||||
|
|
@ -1,6 +1,5 @@
|
|||
-------
|
||||
Scripts
|
||||
-------
|
||||
=======
|
||||
|
||||
This directory contains a number of more or less trivial utilities
|
||||
and demo programs.
|
||||
|
@ -9,50 +8,50 @@ Comments and contributions are welcome.
|
|||
|
||||
</F>
|
||||
|
||||
--------------------------------------------------------------------
|
||||
pildriver.py (by Eric S. Raymond)
|
||||
--------------------------------------------------------------------
|
||||
|
||||
A class implementing an image-processing calculator for scripts.
|
||||
Parses lists of commnds (or, called interactively, command-line
|
||||
arguments) into image loads, transformations, and saves.
|
||||
|
||||
--------------------------------------------------------------------
|
||||
viewer.py
|
||||
--------------------------------------------------------------------
|
||||
|
||||
A simple image viewer. Can display all file formats handled by
|
||||
PIL. Transparent images are properly handled.
|
||||
|
||||
--------------------------------------------------------------------
|
||||
thresholder.py
|
||||
--------------------------------------------------------------------
|
||||
|
||||
A simple utility that demonstrates how a transparent 1-bit overlay
|
||||
can be used to show the current thresholding of an 8-bit image.
|
||||
|
||||
--------------------------------------------------------------------
|
||||
enhancer.py
|
||||
--------------------------------------------------------------------
|
||||
|
||||
Illustrates the ImageEnhance module. Drag the sliders to modify the
|
||||
images. This might be very slow on some platforms, depending on the
|
||||
Tk version.
|
||||
|
||||
--------------------------------------------------------------------
|
||||
painter.py
|
||||
--------------------------------------------------------------------
|
||||
|
||||
Illustrates how a painting program could be based on PIL and Tk.
|
||||
Press the left mouse button and drag over the image to remove the
|
||||
colour. Some clever tricks have been used to get decent performance
|
||||
when updating the screen; see the sources for details.
|
||||
|
||||
--------------------------------------------------------------------
|
||||
player.py
|
||||
--------------------------------------------------------------------
|
||||
|
||||
A simple image sequence player. You can use either a sequence format
|
||||
like FLI/FLC, GIF, or ARG, or give a number of images which are
|
||||
interpreted as frames in a sequence. All frames must have the same
|
||||
size.
|
||||
|
||||
--------------------------------------------------------------------
|
||||
gifmaker.py
|
||||
--------------------------------------------------------------------
|
||||
|
||||
Convert a sequence file to a GIF animation.
|
||||
|
||||
|
@ -60,20 +59,20 @@ Note that the GIF encoder provided with this release of PIL writes
|
|||
uncompressed GIF files only, so the resulting animations are rather
|
||||
large compared with these created by other tools.
|
||||
|
||||
--------------------------------------------------------------------
|
||||
explode.py
|
||||
--------------------------------------------------------------------
|
||||
|
||||
Split a sequence file into individual frames.
|
||||
|
||||
--------------------------------------------------------------------
|
||||
image2py.py
|
||||
--------------------------------------------------------------------
|
||||
|
||||
Convert an image to a Python module containing an IMAGE variable.
|
||||
Note that the module using the module must include JPEG and ZIP
|
||||
decoders, unless the -u option is used.
|
||||
|
||||
--------------------------------------------------------------------
|
||||
olesummary.py
|
||||
--------------------------------------------------------------------
|
||||
|
||||
Uses the OleFileIO module to dump the summary information from an OLE
|
||||
structured storage file. This works with most OLE files, including
|
16
Scripts/createfontdatachunk.py
Normal file
|
@ -0,0 +1,16 @@
|
|||
import base64
|
||||
import os
|
||||
import sys
|
||||
|
||||
if __name__ == "__main__":
|
||||
# create font data chunk for embedding
|
||||
font = "Tests/images/courB08"
|
||||
print(" f._load_pilfont_data(")
|
||||
print(" # %s" % os.path.basename(font))
|
||||
print(" BytesIO(base64.decodestring(b'''")
|
||||
base64.encode(open(font + ".pil", "rb"), sys.stdout)
|
||||
print("''')), Image.open(BytesIO(base64.decodestring(b'''")
|
||||
base64.encode(open(font + ".pbm", "rb"), sys.stdout)
|
||||
print("'''))))")
|
||||
|
||||
# End of file
|
7
Scripts/diffcover-install.sh
Executable file
|
@ -0,0 +1,7 @@
|
|||
# Fetch the remote master branch before running diff-cover on Travis CI.
|
||||
# https://github.com/edx/diff-cover#troubleshooting
|
||||
git fetch origin master:refs/remotes/origin/master
|
||||
|
||||
# CFLAGS=-O0 means build with no optimisation.
|
||||
# Makes build much quicker for lxml and other dependencies.
|
||||
time CFLAGS=-O0 pip install --use-wheel diff_cover
|
4
Scripts/diffcover-run.sh
Executable file
|
@ -0,0 +1,4 @@
|
|||
coverage xml
|
||||
diff-cover coverage.xml
|
||||
diff-quality --violation=pyflakes
|
||||
diff-quality --violation=pep8
|
|
@ -104,7 +104,7 @@ while True:
|
|||
except EOFError:
|
||||
break
|
||||
|
||||
ix = ix + 1
|
||||
ix += 1
|
||||
|
||||
if html:
|
||||
html.write("</body>\n</html>\n")
|
||||
|
|
|
@ -100,7 +100,7 @@ def makedelta(fp, sequence):
|
|||
|
||||
previous = im.copy()
|
||||
|
||||
frames = frames + 1
|
||||
frames += 1
|
||||
|
||||
fp.write(";")
|
||||
|
||||
|
|
|
@ -486,7 +486,7 @@ class PILDriver:
|
|||
print("Stack: " + repr(self.stack))
|
||||
top = self.top()
|
||||
if not isinstance(top, str):
|
||||
continue;
|
||||
continue
|
||||
funcname = "do_" + top
|
||||
if not hasattr(self, funcname):
|
||||
continue
|
||||
|
@ -513,9 +513,9 @@ if __name__ == '__main__':
|
|||
while True:
|
||||
try:
|
||||
if sys.version_info[0] >= 3:
|
||||
line = input('pildriver> ');
|
||||
line = input('pildriver> ')
|
||||
else:
|
||||
line = raw_input('pildriver> ');
|
||||
line = raw_input('pildriver> ')
|
||||
except EOFError:
|
||||
print("\nPILDriver says goodbye.")
|
||||
break
|
||||
|
|
|
@ -57,7 +57,7 @@ for o, a in opt:
|
|||
elif o == "-v":
|
||||
verify = 1
|
||||
elif o == "-D":
|
||||
Image.DEBUG = Image.DEBUG + 1
|
||||
Image.DEBUG += 1
|
||||
|
||||
def globfix(files):
|
||||
# expand wildcards where necessary
|
||||
|
|
|
@ -18,25 +18,6 @@ import sys
|
|||
Image.DEBUG = 0
|
||||
|
||||
|
||||
# --------------------------------------------------------------------
|
||||
# experimental: support ARG animation scripts
|
||||
|
||||
import ArgImagePlugin
|
||||
|
||||
def applet_hook(animation, images):
|
||||
app = animation(animation_display, images)
|
||||
app.run()
|
||||
|
||||
ArgImagePlugin.APPLET_HOOK = applet_hook
|
||||
|
||||
class AppletDisplay:
|
||||
def __init__(self, ui):
|
||||
self.__ui = ui
|
||||
def paste(self, im, bbox):
|
||||
self.__ui.image.paste(im, bbox)
|
||||
def update(self):
|
||||
self.__ui.update_idletasks()
|
||||
|
||||
# --------------------------------------------------------------------
|
||||
# an image animation player
|
||||
|
||||
|
@ -56,10 +37,6 @@ class UI(Label):
|
|||
else:
|
||||
self.image = ImageTk.PhotoImage(im)
|
||||
|
||||
# APPLET SUPPORT (very crude, and not 100% safe)
|
||||
global animation_display
|
||||
animation_display = AppletDisplay(self)
|
||||
|
||||
Label.__init__(self, master, image=self.image, bg="black", bd=0)
|
||||
|
||||
self.update()
|
||||
|
|
10
Tests/32bit_segfault_check.py
Normal file
|
@ -0,0 +1,10 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
from PIL import Image
|
||||
import sys
|
||||
|
||||
|
||||
if sys.maxsize < 2**32:
|
||||
im = Image.new('L', (999999, 999999), 0)
|
||||
|
||||
|
45
Tests/README.rst
Normal file
|
@ -0,0 +1,45 @@
|
|||
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
|
||||
|
||||
|
||||
Execution
|
||||
---------
|
||||
|
||||
**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
|
||||
|
||||
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
Minimalistic PIL test framework.
|
||||
|
||||
Test scripts are named "test_xxx" and are supposed to output "ok". That's it. To run the tests::
|
||||
|
||||
python setup.py develop
|
||||
|
||||
Run the tests from the root of the Pillow source distribution:
|
||||
|
||||
python selftest.py
|
||||
python Tests/run.py --installed
|
||||
|
||||
To run an individual test:
|
||||
|
||||
python Tests/test_image.py
|
|
@ -1,23 +1,25 @@
|
|||
from tester import *
|
||||
from helper import *
|
||||
|
||||
# not running this test by default. No DOS against travis.
|
||||
# Not running this test by default. No DOS against Travis CI.
|
||||
|
||||
from PIL import PyAccess
|
||||
from PIL import Image
|
||||
|
||||
import time
|
||||
|
||||
|
||||
def iterate_get(size, access):
|
||||
(w,h) = size
|
||||
(w, h) = size
|
||||
for x in range(w):
|
||||
for y in range(h):
|
||||
access[(x,y)]
|
||||
access[(x, y)]
|
||||
|
||||
|
||||
def iterate_set(size, access):
|
||||
(w,h) = size
|
||||
(w, h) = size
|
||||
for x in range(w):
|
||||
for y in range(h):
|
||||
access[(x,y)] = (x %256,y%256,0)
|
||||
access[(x, y)] = (x % 256, y % 256, 0)
|
||||
|
||||
|
||||
def timer(func, label, *args):
|
||||
iterations = 5000
|
||||
|
@ -25,27 +27,34 @@ def timer(func, label, *args):
|
|||
for x in range(iterations):
|
||||
func(*args)
|
||||
if time.time()-starttime > 10:
|
||||
print ("%s: breaking at %s iterations, %.6f per iteration"%(label, x+1, (time.time()-starttime)/(x+1.0)))
|
||||
print("%s: breaking at %s iterations, %.6f per iteration" % (
|
||||
label, x+1, (time.time()-starttime)/(x+1.0)))
|
||||
break
|
||||
if x == iterations-1:
|
||||
endtime = time.time()
|
||||
print ("%s: %.4f s %.6f per iteration" %(label, endtime-starttime, (endtime-starttime)/(x+1.0)))
|
||||
print("%s: %.4f s %.6f per iteration" % (
|
||||
label, endtime-starttime, (endtime-starttime)/(x+1.0)))
|
||||
|
||||
def test_direct():
|
||||
im = lena()
|
||||
im.load()
|
||||
#im = Image.new( "RGB", (2000,2000), (1,3,2))
|
||||
caccess = im.im.pixel_access(False)
|
||||
access = PyAccess.new(im, False)
|
||||
|
||||
assert_equal(caccess[(0,0)], access[(0,0)])
|
||||
class BenchCffiAccess(PillowTestCase):
|
||||
|
||||
print ("Size: %sx%s" % im.size)
|
||||
timer(iterate_get, 'PyAccess - get', im.size, access)
|
||||
timer(iterate_set, 'PyAccess - set', im.size, access)
|
||||
timer(iterate_get, 'C-api - get', im.size, caccess)
|
||||
timer(iterate_set, 'C-api - set', im.size, caccess)
|
||||
|
||||
|
||||
def test_direct(self):
|
||||
im = lena()
|
||||
im.load()
|
||||
# im = Image.new( "RGB", (2000, 2000), (1, 3, 2))
|
||||
caccess = im.im.pixel_access(False)
|
||||
access = PyAccess.new(im, False)
|
||||
|
||||
|
||||
self.assertEqual(caccess[(0, 0)], access[(0, 0)])
|
||||
|
||||
print ("Size: %sx%s" % im.size)
|
||||
timer(iterate_get, 'PyAccess - get', im.size, access)
|
||||
timer(iterate_set, 'PyAccess - set', im.size, access)
|
||||
timer(iterate_get, 'C-api - get', im.size, caccess)
|
||||
timer(iterate_set, 'C-api - set', im.size, caccess)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
||||
# End of file
|
||||
|
|
|
@ -1,13 +1,14 @@
|
|||
import sys
|
||||
sys.path.insert(0, ".")
|
||||
|
||||
import tester
|
||||
import helper
|
||||
import timeit
|
||||
|
||||
|
||||
def bench(mode):
|
||||
im = tester.lena(mode)
|
||||
im = helper.lena(mode)
|
||||
get = im.im.getpixel
|
||||
xy = 50, 50 # position shouldn't really matter
|
||||
xy = 50, 50 # position shouldn't really matter
|
||||
t0 = timeit.default_timer()
|
||||
for i in range(1000000):
|
||||
get(xy)
|
||||
|
|
10
Tests/check_icns_dos.py
Normal file
|
@ -0,0 +1,10 @@
|
|||
# Tests potential DOS of IcnsImagePlugin with 0 length block.
|
||||
# Run from anywhere that PIL is importable.
|
||||
|
||||
from PIL import Image
|
||||
from io import BytesIO
|
||||
|
||||
if bytes is str:
|
||||
Image.open(BytesIO(bytes('icns\x00\x00\x00\x10hang\x00\x00\x00\x00')))
|
||||
else:
|
||||
Image.open(BytesIO(bytes('icns\x00\x00\x00\x10hang\x00\x00\x00\x00', 'latin-1')))
|
11
Tests/check_j2k_dos.py
Normal file
|
@ -0,0 +1,11 @@
|
|||
# Tests potential DOS of Jpeg2kImagePlugin with 0 length block.
|
||||
# Run from anywhere that PIL is importable.
|
||||
|
||||
from PIL import Image
|
||||
from io import BytesIO
|
||||
|
||||
if bytes is str:
|
||||
Image.open(BytesIO(bytes('\x00\x00\x00\x0cjP\x20\x20\x0d\x0a\x87\x0a\x00\x00\x00\x00hang')))
|
||||
else:
|
||||
Image.open(BytesIO(bytes('\x00\x00\x00\x0cjP\x20\x20\x0d\x0a\x87\x0a\x00\x00\x00\x00hang', 'latin-1')))
|
||||
|
|
@ -1,222 +0,0 @@
|
|||
# PyCMSTests.py
|
||||
# Examples of how to use pyCMS, as well as tests to verify it works properly
|
||||
# By Kevin Cazabon (kevin@cazabon.com)
|
||||
|
||||
# Imports
|
||||
import os
|
||||
from PIL import Image
|
||||
from PIL import ImageCms
|
||||
|
||||
# import PyCMSError separately so we can catch it
|
||||
PyCMSError = ImageCms.PyCMSError
|
||||
|
||||
#######################################################################
|
||||
# Configuration:
|
||||
#######################################################################
|
||||
# set this to the image you want to test with
|
||||
IMAGE = "c:\\temp\\test.tif"
|
||||
|
||||
# set this to where you want to save the output images
|
||||
OUTPUTDIR = "c:\\temp\\"
|
||||
|
||||
# set these to two different ICC profiles, one for input, one for output
|
||||
# set the corresponding mode to the proper PIL mode for that profile
|
||||
INPUT_PROFILE = "c:\\temp\\profiles\\sRGB.icm"
|
||||
INMODE = "RGB"
|
||||
|
||||
OUTPUT_PROFILE = "c:\\temp\\profiles\\genericRGB.icm"
|
||||
OUTMODE = "RGB"
|
||||
|
||||
PROOF_PROFILE = "c:\\temp\\profiles\\monitor.icm"
|
||||
|
||||
# set to True to show() images, False to save them into OUTPUT_DIRECTORY
|
||||
SHOW = False
|
||||
|
||||
# Tests you can enable/disable
|
||||
TEST_error_catching = True
|
||||
TEST_profileToProfile = True
|
||||
TEST_profileToProfile_inPlace = True
|
||||
TEST_buildTransform = True
|
||||
TEST_buildTransformFromOpenProfiles = True
|
||||
TEST_buildProofTransform = True
|
||||
TEST_getProfileInfo = True
|
||||
TEST_misc = False
|
||||
|
||||
#######################################################################
|
||||
# helper functions
|
||||
#######################################################################
|
||||
def outputImage(im, funcName = None):
|
||||
# save or display the image, depending on value of SHOW_IMAGES
|
||||
if SHOW:
|
||||
im.show()
|
||||
else:
|
||||
im.save(os.path.join(OUTPUTDIR, "%s.tif" %funcName))
|
||||
|
||||
|
||||
#######################################################################
|
||||
# The tests themselves
|
||||
#######################################################################
|
||||
|
||||
if TEST_error_catching:
|
||||
im = Image.open(IMAGE)
|
||||
try:
|
||||
#neither of these proifles exists (unless you make them), so we should
|
||||
# get an error
|
||||
imOut = ImageCms.profileToProfile(im, "missingProfile.icm", "cmyk.icm")
|
||||
|
||||
except PyCMSError as reason:
|
||||
print("We caught a PyCMSError: %s\n\n" %reason)
|
||||
|
||||
print("error catching test completed successfully (if you see the message \
|
||||
above that we caught the error).")
|
||||
|
||||
if TEST_profileToProfile:
|
||||
# open the image file using the standard PIL function Image.open()
|
||||
im = Image.open(IMAGE)
|
||||
|
||||
# send the image, input/output profiles, and rendering intent to
|
||||
# ImageCms.profileToProfile()
|
||||
imOut = ImageCms.profileToProfile(im, INPUT_PROFILE, OUTPUT_PROFILE, \
|
||||
outputMode = OUTMODE)
|
||||
|
||||
# now that the image is converted, save or display it
|
||||
outputImage(imOut, "profileToProfile")
|
||||
|
||||
print("profileToProfile test completed successfully.")
|
||||
|
||||
if TEST_profileToProfile_inPlace:
|
||||
# we'll do the same test as profileToProfile, but modify im in place
|
||||
# instead of getting a new image returned to us
|
||||
im = Image.open(IMAGE)
|
||||
|
||||
# send the image to ImageCms.profileToProfile(), specifying inPlace = True
|
||||
result = ImageCms.profileToProfile(im, INPUT_PROFILE, OUTPUT_PROFILE, \
|
||||
outputMode = OUTMODE, inPlace = True)
|
||||
|
||||
# now that the image is converted, save or display it
|
||||
if result is None:
|
||||
# this is the normal result when modifying in-place
|
||||
outputImage(im, "profileToProfile_inPlace")
|
||||
else:
|
||||
# something failed...
|
||||
print("profileToProfile in-place failed: %s" %result)
|
||||
|
||||
print("profileToProfile in-place test completed successfully.")
|
||||
|
||||
if TEST_buildTransform:
|
||||
# make a transform using the input and output profile path strings
|
||||
transform = ImageCms.buildTransform(INPUT_PROFILE, OUTPUT_PROFILE, INMODE, \
|
||||
OUTMODE)
|
||||
|
||||
# now, use the trnsform to convert a couple images
|
||||
im = Image.open(IMAGE)
|
||||
|
||||
# transform im normally
|
||||
im2 = ImageCms.applyTransform(im, transform)
|
||||
outputImage(im2, "buildTransform")
|
||||
|
||||
# then transform it again using the same transform, this time in-place.
|
||||
result = ImageCms.applyTransform(im, transform, inPlace = True)
|
||||
outputImage(im, "buildTransform_inPlace")
|
||||
|
||||
print("buildTransform test completed successfully.")
|
||||
|
||||
# and, to clean up a bit, delete the transform
|
||||
# this should call the C destructor for the transform structure.
|
||||
# Python should also do this automatically when it goes out of scope.
|
||||
del(transform)
|
||||
|
||||
if TEST_buildTransformFromOpenProfiles:
|
||||
# we'll actually test a couple profile open/creation functions here too
|
||||
|
||||
# first, get a handle to an input profile, in this case we'll create
|
||||
# an sRGB profile on the fly:
|
||||
inputProfile = ImageCms.createProfile("sRGB")
|
||||
|
||||
# then, get a handle to the output profile
|
||||
outputProfile = ImageCms.getOpenProfile(OUTPUT_PROFILE)
|
||||
|
||||
# make a transform from these
|
||||
transform = ImageCms.buildTransformFromOpenProfiles(inputProfile, \
|
||||
outputProfile, INMODE, OUTMODE)
|
||||
|
||||
# now, use the trnsform to convert a couple images
|
||||
im = Image.open(IMAGE)
|
||||
|
||||
# transform im normally
|
||||
im2 = ImageCms.applyTransform(im, transform)
|
||||
outputImage(im2, "buildTransformFromOpenProfiles")
|
||||
|
||||
# then do it again using the same transform, this time in-place.
|
||||
result = ImageCms.applyTransform(im, transform, inPlace = True)
|
||||
outputImage(im, "buildTransformFromOpenProfiles_inPlace")
|
||||
|
||||
print("buildTransformFromOpenProfiles test completed successfully.")
|
||||
|
||||
# and, to clean up a bit, delete the transform
|
||||
# this should call the C destructor for the each item.
|
||||
# Python should also do this automatically when it goes out of scope.
|
||||
del(inputProfile)
|
||||
del(outputProfile)
|
||||
del(transform)
|
||||
|
||||
if TEST_buildProofTransform:
|
||||
# make a transform using the input and output and proof profile path
|
||||
# strings
|
||||
# images converted with this transform will simulate the appearance
|
||||
# of the output device while actually being displayed/proofed on the
|
||||
# proof device. This usually means a monitor, but can also mean
|
||||
# other proof-printers like dye-sub, etc.
|
||||
transform = ImageCms.buildProofTransform(INPUT_PROFILE, OUTPUT_PROFILE, \
|
||||
PROOF_PROFILE, INMODE, OUTMODE)
|
||||
|
||||
# now, use the trnsform to convert a couple images
|
||||
im = Image.open(IMAGE)
|
||||
|
||||
# transform im normally
|
||||
im2 = ImageCms.applyTransform(im, transform)
|
||||
outputImage(im2, "buildProofTransform")
|
||||
|
||||
# then transform it again using the same transform, this time in-place.
|
||||
result = ImageCms.applyTransform(im, transform, inPlace = True)
|
||||
outputImage(im, "buildProofTransform_inPlace")
|
||||
|
||||
print("buildProofTransform test completed successfully.")
|
||||
|
||||
# and, to clean up a bit, delete the transform
|
||||
# this should call the C destructor for the transform structure.
|
||||
# Python should also do this automatically when it goes out of scope.
|
||||
del(transform)
|
||||
|
||||
if TEST_getProfileInfo:
|
||||
# get a profile handle
|
||||
profile = ImageCms.getOpenProfile(INPUT_PROFILE)
|
||||
|
||||
# lets print some info about our input profile:
|
||||
print("Profile name (retrieved from profile string path name): %s" %ImageCms.getProfileName(INPUT_PROFILE))
|
||||
|
||||
# or, you could do the same thing using a profile handle as the arg
|
||||
print("Profile name (retrieved from profile handle): %s" %ImageCms.getProfileName(profile))
|
||||
|
||||
# now lets get the embedded "info" tag contents
|
||||
# once again, you can use a path to a profile, or a profile handle
|
||||
print("Profile info (retrieved from profile handle): %s" %ImageCms.getProfileInfo(profile))
|
||||
|
||||
# and what's the default intent of this profile?
|
||||
print("The default intent is (this will be an integer): %d" %(ImageCms.getDefaultIntent(profile)))
|
||||
|
||||
# Hmmmm... but does this profile support INTENT_ABSOLUTE_COLORIMETRIC?
|
||||
print("Does it support INTENT_ABSOLUTE_COLORIMETRIC?: (1 is yes, -1 is no): %s" \
|
||||
%ImageCms.isIntentSupported(profile, ImageCms.INTENT_ABSOLUTE_COLORIMETRIC, \
|
||||
ImageCms.DIRECTION_INPUT))
|
||||
|
||||
print("getProfileInfo test completed successfully.")
|
||||
|
||||
if TEST_misc:
|
||||
# test the versions, about, and copyright functions
|
||||
print("Versions: %s" %str(ImageCms.versions()))
|
||||
print("About:\n\n%s" %ImageCms.about())
|
||||
print("Copyright:\n\n%s" %ImageCms.copyright())
|
||||
|
||||
print("misc test completed successfully.")
|
||||
|
234
Tests/helper.py
Normal file
|
@ -0,0 +1,234 @@
|
|||
"""
|
||||
Helper functions.
|
||||
"""
|
||||
from __future__ import print_function
|
||||
import sys
|
||||
import tempfile
|
||||
import os
|
||||
|
||||
if sys.version_info[:2] <= (2, 6):
|
||||
import unittest2 as unittest
|
||||
else:
|
||||
import unittest
|
||||
|
||||
|
||||
class PillowTestCase(unittest.TestCase):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
unittest.TestCase.__init__(self, *args, **kwargs)
|
||||
# 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
|
||||
unittest.TestCase.run(self, result) # call superclass run method
|
||||
|
||||
def delete_tempfile(self, path):
|
||||
try:
|
||||
ok = self.currentResult.wasSuccessful()
|
||||
except AttributeError: # for nosetests
|
||||
proxy = self.currentResult
|
||||
ok = (len(proxy.errors) + len(proxy.failures) == 0)
|
||||
|
||||
if ok:
|
||||
# only clean out tempfiles if test passed
|
||||
try:
|
||||
os.remove(path)
|
||||
except OSError:
|
||||
pass # report?
|
||||
else:
|
||||
print("=== orphaned temp file: %s" % path)
|
||||
|
||||
def assert_almost_equal(self, a, b, msg=None, eps=1e-6):
|
||||
self.assertLess(
|
||||
abs(a-b), eps,
|
||||
msg or "got %r, expected %r" % (a, b))
|
||||
|
||||
def assert_deep_equal(self, a, b, msg=None):
|
||||
try:
|
||||
self.assertEqual(
|
||||
len(a), len(b),
|
||||
msg or "got length %s, expected %s" % (len(a), len(b)))
|
||||
self.assertTrue(
|
||||
all([x == y for x, y in zip(a, b)]),
|
||||
msg or "got %s, expected %s" % (a, b))
|
||||
except:
|
||||
self.assertEqual(a, b, msg)
|
||||
|
||||
def assert_image(self, im, mode, size, msg=None):
|
||||
if mode is not None:
|
||||
self.assertEqual(
|
||||
im.mode, mode,
|
||||
msg or "got mode %r, expected %r" % (im.mode, mode))
|
||||
|
||||
if size is not None:
|
||||
self.assertEqual(
|
||||
im.size, size,
|
||||
msg or "got size %r, expected %r" % (im.size, size))
|
||||
|
||||
def assert_image_equal(self, a, b, msg=None):
|
||||
self.assertEqual(
|
||||
a.mode, b.mode,
|
||||
msg or "got mode %r, expected %r" % (a.mode, b.mode))
|
||||
self.assertEqual(
|
||||
a.size, b.size,
|
||||
msg or "got size %r, expected %r" % (a.size, b.size))
|
||||
if a.tobytes() != b.tobytes():
|
||||
self.fail(msg or "got different content")
|
||||
|
||||
def assert_image_similar(self, a, b, epsilon, msg=None):
|
||||
epsilon = float(epsilon)
|
||||
self.assertEqual(
|
||||
a.mode, b.mode,
|
||||
msg or "got mode %r, expected %r" % (a.mode, b.mode))
|
||||
self.assertEqual(
|
||||
a.size, b.size,
|
||||
msg or "got size %r, expected %r" % (a.size, b.size))
|
||||
|
||||
diff = 0
|
||||
try:
|
||||
ord(b'0')
|
||||
for abyte, bbyte in zip(a.tobytes(), b.tobytes()):
|
||||
diff += abs(ord(abyte)-ord(bbyte))
|
||||
except:
|
||||
for abyte, bbyte in zip(a.tobytes(), b.tobytes()):
|
||||
diff += abs(abyte-bbyte)
|
||||
ave_diff = float(diff)/(a.size[0]*a.size[1])
|
||||
self.assertGreaterEqual(
|
||||
epsilon, ave_diff,
|
||||
(msg or '') + " average pixel value difference %.4f > epsilon %.4f" % (
|
||||
ave_diff, epsilon))
|
||||
|
||||
def assert_warning(self, warn_class, func):
|
||||
import warnings
|
||||
|
||||
result = None
|
||||
with warnings.catch_warnings(record=True) as w:
|
||||
# Cause all warnings to always be triggered.
|
||||
warnings.simplefilter("always")
|
||||
|
||||
# Hopefully trigger a warning.
|
||||
result = func()
|
||||
|
||||
# Verify some things.
|
||||
self.assertGreaterEqual(len(w), 1)
|
||||
found = False
|
||||
for v in w:
|
||||
if issubclass(v.category, warn_class):
|
||||
found = True
|
||||
break
|
||||
self.assertTrue(found)
|
||||
return result
|
||||
|
||||
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)):
|
||||
print (os.environ.get('PILLOW_RUN_KNOWN_BAD', False))
|
||||
return
|
||||
|
||||
skip = True
|
||||
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)))
|
||||
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")
|
||||
|
||||
def tempfile(self, template):
|
||||
assert template[:5] in ("temp.", "temp_")
|
||||
(fd, path) = tempfile.mkstemp(template[4:], template[:4])
|
||||
os.close(fd)
|
||||
|
||||
self.addCleanup(self.delete_tempfile, path)
|
||||
return path
|
||||
|
||||
def open_withImagemagick(self, f):
|
||||
if not imagemagick_available():
|
||||
raise IOError()
|
||||
|
||||
outfile = self.tempfile("temp.png")
|
||||
if command_succeeds(['convert', f, outfile]):
|
||||
from PIL import Image
|
||||
return Image.open(outfile)
|
||||
raise IOError()
|
||||
|
||||
|
||||
# helpers
|
||||
|
||||
import sys
|
||||
py3 = (sys.version_info >= (3, 0))
|
||||
|
||||
|
||||
def fromstring(data):
|
||||
from io import BytesIO
|
||||
from PIL import Image
|
||||
return Image.open(BytesIO(data))
|
||||
|
||||
|
||||
def tostring(im, format, **options):
|
||||
from io import BytesIO
|
||||
out = BytesIO()
|
||||
im.save(out, format, **options)
|
||||
return out.getvalue()
|
||||
|
||||
|
||||
def lena(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/lena.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 command_succeeds(cmd):
|
||||
"""
|
||||
Runs the command, which must be a list of strings. Returns True if the
|
||||
command succeeds, or False if an OSError was raised by subprocess.Popen.
|
||||
"""
|
||||
import os
|
||||
import subprocess
|
||||
with open(os.devnull, 'w') as f:
|
||||
try:
|
||||
subprocess.Popen(cmd, stdout=f, stderr=subprocess.STDOUT).wait()
|
||||
except OSError:
|
||||
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"]))
|
||||
|
||||
|
||||
def imagemagick_available():
|
||||
return command_succeeds(['convert', '-version'])
|
||||
|
||||
# End of file
|
BIN
Tests/images/16bit.cropped.j2k
Normal file
BIN
Tests/images/16bit.cropped.jp2
Normal file
BIN
Tests/images/corner.lut
Normal file
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
BIN
Tests/images/deerstalker.cur
Normal file
After Width: | Height: | Size: 4.2 KiB |
BIN
Tests/images/default_font.png
Normal file
After Width: | Height: | Size: 586 B |
BIN
Tests/images/dilation4.lut
Normal file
BIN
Tests/images/dilation8.lut
Normal file
BIN
Tests/images/dispose_bgnd.gif
Normal file
After Width: | Height: | Size: 1.4 KiB |
BIN
Tests/images/dispose_none.gif
Normal file
After Width: | Height: | Size: 1.4 KiB |
BIN
Tests/images/dispose_prev.gif
Normal file
After Width: | Height: | Size: 1.4 KiB |
BIN
Tests/images/edge.lut
Normal file
BIN
Tests/images/erosion4.lut
Normal file
BIN
Tests/images/erosion8.lut
Normal file
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 29 KiB |
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 21 KiB |
BIN
Tests/images/frozenpond.mpo
Normal file
After Width: | Height: | Size: 162 KiB |
BIN
Tests/images/imagedraw/line_horizontal_slope1px_w2px.png
Normal file
After Width: | Height: | Size: 147 B |
BIN
Tests/images/imagedraw/line_horizontal_w101px.png
Normal file
After Width: | Height: | Size: 368 B |
BIN
Tests/images/imagedraw/line_horizontal_w2px_inverted.png
Normal file
After Width: | Height: | Size: 143 B |
BIN
Tests/images/imagedraw/line_horizontal_w2px_normal.png
Normal file
After Width: | Height: | Size: 141 B |