Merge branch 'master' into tiff_exif
|
@ -22,6 +22,7 @@ sudo apt-get -qq install libfreetype6-dev liblcms2-dev python3-tk\
|
||||||
cmake imagemagick libharfbuzz-dev libfribidi-dev
|
cmake imagemagick libharfbuzz-dev libfribidi-dev
|
||||||
|
|
||||||
python3 -m pip install --upgrade pip
|
python3 -m pip install --upgrade pip
|
||||||
|
python3 -m pip install --upgrade wheel
|
||||||
PYTHONOPTIMIZE=0 python3 -m pip install cffi
|
PYTHONOPTIMIZE=0 python3 -m pip install cffi
|
||||||
python3 -m pip install coverage
|
python3 -m pip install coverage
|
||||||
python3 -m pip install defusedxml
|
python3 -m pip install defusedxml
|
||||||
|
@ -31,12 +32,10 @@ python3 -m pip install -U pytest-cov
|
||||||
python3 -m pip install -U pytest-timeout
|
python3 -m pip install -U pytest-timeout
|
||||||
python3 -m pip install pyroma
|
python3 -m pip install pyroma
|
||||||
python3 -m pip install test-image-results
|
python3 -m pip install test-image-results
|
||||||
# TODO Remove condition when numpy supports 3.10
|
python3 -m pip install numpy
|
||||||
if ! [ "$GHA_PYTHON_VERSION" == "3.10-dev" ]; then python3 -m pip install numpy ; fi
|
|
||||||
|
|
||||||
# PyQt5 doesn't support PyPy3
|
# PyQt5 doesn't support PyPy3
|
||||||
# Wheel doesn't yet support 3.10
|
if [[ $GHA_PYTHON_VERSION == 3.* ]]; then
|
||||||
if [[ $GHA_PYTHON_VERSION == 3.* && $GHA_PYTHON_VERSION != "3.10-dev" ]]; then
|
|
||||||
# arm64, ppc64le, s390x CPUs:
|
# arm64, ppc64le, s390x CPUs:
|
||||||
# "ERROR: Could not find a version that satisfies the requirement pyqt5"
|
# "ERROR: Could not find a version that satisfies the requirement pyqt5"
|
||||||
sudo apt-get -qq install libxcb-xinerama0 pyqt5-dev-tools
|
sudo apt-get -qq install libxcb-xinerama0 pyqt5-dev-tools
|
||||||
|
|
3
.github/workflows/macos-install.sh
vendored
|
@ -15,8 +15,7 @@ python3 -m pip install pyroma
|
||||||
python3 -m pip install test-image-results
|
python3 -m pip install test-image-results
|
||||||
|
|
||||||
echo -e "[openblas]\nlibraries = openblas\nlibrary_dirs = /usr/local/opt/openblas/lib" >> ~/.numpy-site.cfg
|
echo -e "[openblas]\nlibraries = openblas\nlibrary_dirs = /usr/local/opt/openblas/lib" >> ~/.numpy-site.cfg
|
||||||
# TODO Remove condition when numpy supports 3.10
|
python3 -m pip install numpy
|
||||||
if ! [ "$GHA_PYTHON_VERSION" == "3.10-dev" ]; then python3 -m pip install numpy ; fi
|
|
||||||
|
|
||||||
# extra test images
|
# extra test images
|
||||||
pushd depends && ./install_extra_test_images.sh && popd
|
pushd depends && ./install_extra_test_images.sh && popd
|
||||||
|
|
2
.github/workflows/test.yml
vendored
|
@ -72,8 +72,6 @@ jobs:
|
||||||
if: startsWith(matrix.os, 'macOS')
|
if: startsWith(matrix.os, 'macOS')
|
||||||
run: |
|
run: |
|
||||||
.github/workflows/macos-install.sh
|
.github/workflows/macos-install.sh
|
||||||
env:
|
|
||||||
GHA_PYTHON_VERSION: ${{ matrix.python-version }}
|
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: |
|
run: |
|
||||||
|
|
1
.gitignore
vendored
|
@ -83,6 +83,7 @@ docs/_build/
|
||||||
Tests/images/README.md
|
Tests/images/README.md
|
||||||
Tests/images/crash_1.tif
|
Tests/images/crash_1.tif
|
||||||
Tests/images/crash_2.tif
|
Tests/images/crash_2.tif
|
||||||
|
Tests/images/crash-81154a65438ba5aaeca73fd502fa4850fbde60f8.tif
|
||||||
Tests/images/string_dimension.tiff
|
Tests/images/string_dimension.tiff
|
||||||
Tests/images/jpeg2000
|
Tests/images/jpeg2000
|
||||||
Tests/images/msp
|
Tests/images/msp
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/psf/black
|
- repo: https://github.com/psf/black
|
||||||
rev: e66be67b9b6811913470f70c28b4d50f94d05b22 # frozen: 20.8b1
|
rev: e3000ace2fd1fcb1c181bb7a8285f1f976bcbdc7 # frozen: 21.7b0
|
||||||
hooks:
|
hooks:
|
||||||
- id: black
|
- id: black
|
||||||
args: ["--target-version", "py36"]
|
args: ["--target-version", "py36"]
|
||||||
|
@ -9,35 +9,38 @@ repos:
|
||||||
types: []
|
types: []
|
||||||
|
|
||||||
- repo: https://github.com/PyCQA/isort
|
- repo: https://github.com/PyCQA/isort
|
||||||
rev: 377d260ffa6f746693f97b46d95025afc4bd8275 # frozen: 5.4.2
|
rev: fd5ba70665a37ec301a1f714ed09336048b3be63 # frozen: 5.9.3
|
||||||
hooks:
|
hooks:
|
||||||
- id: isort
|
- id: isort
|
||||||
|
|
||||||
- repo: https://github.com/asottile/yesqa
|
- repo: https://github.com/asottile/yesqa
|
||||||
rev: 7a009f3ee493c796827ee334f9058b110a0e0db8 # frozen: v1.2.1
|
rev: 644ede78511c02fc6f8e03e014cc1ddcfbf1e1f5 # frozen: v1.2.3
|
||||||
hooks:
|
hooks:
|
||||||
- id: yesqa
|
- id: yesqa
|
||||||
|
|
||||||
- repo: https://github.com/Lucas-C/pre-commit-hooks
|
- repo: https://github.com/Lucas-C/pre-commit-hooks
|
||||||
rev: f30f4974a08a6b2f6a1eeaf30a4d501cf909163a # frozen: v1.1.9
|
rev: 3592548bbd98528887eeed63486cf6c9bae00b98 # frozen: v1.1.10
|
||||||
hooks:
|
hooks:
|
||||||
- id: remove-tabs
|
- id: remove-tabs
|
||||||
exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.opt$)
|
exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.opt$)
|
||||||
|
|
||||||
- repo: https://gitlab.com/pycqa/flake8
|
- repo: https://gitlab.com/pycqa/flake8
|
||||||
rev: 05f6544aef321e2fee03a1277ce2eef8880fb927 # frozen: 3.8.3
|
rev: dcd740bc0ebaf2b3d43e59a0060d157c97de13f3 # frozen: 3.9.2
|
||||||
hooks:
|
hooks:
|
||||||
- id: flake8
|
- id: flake8
|
||||||
additional_dependencies: [flake8-2020, flake8-implicit-str-concat]
|
additional_dependencies: [flake8-2020, flake8-implicit-str-concat]
|
||||||
|
|
||||||
- repo: https://github.com/pre-commit/pygrep-hooks
|
- repo: https://github.com/pre-commit/pygrep-hooks
|
||||||
rev: eae6397e4c259ed3d057511f6dd5330b92867e62 # frozen: v1.6.0
|
rev: 6f51a66bba59954917140ec2eeeaa4d5e630e6ce # frozen: v1.9.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: python-check-blanket-noqa
|
- id: python-check-blanket-noqa
|
||||||
- id: rst-backticks
|
- id: rst-backticks
|
||||||
|
|
||||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
rev: e1668fe86af3810fbca72b8653fe478e66a0afdc # frozen: v3.2.0
|
rev: 38b88246ccc552bffaaf54259d064beeee434539 # frozen: v4.0.1
|
||||||
hooks:
|
hooks:
|
||||||
- id: check-merge-conflict
|
- id: check-merge-conflict
|
||||||
- id: check-yaml
|
- id: check-yaml
|
||||||
|
|
||||||
|
ci:
|
||||||
|
autoupdate_schedule: quarterly
|
||||||
|
|
108
CHANGES.rst
|
@ -2,6 +2,114 @@
|
||||||
Changelog (Pillow)
|
Changelog (Pillow)
|
||||||
==================
|
==================
|
||||||
|
|
||||||
|
8.4.0 (unreleased)
|
||||||
|
------------------
|
||||||
|
|
||||||
|
- Copy Python palette to new image in quantize() #5696
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Read ICO AND mask from end #5667
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Actually check the framesize in FliDecode.c #5659
|
||||||
|
[wiredfool]
|
||||||
|
|
||||||
|
- Determine JPEG2000 mode purely from ihdr header box #5654
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Fixed using info dictionary when writing multiple APNG frames #5611
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Allow saving 1 and L mode TIFF with PhotometricInterpretation 0 #5655
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- For GIF save_all with palette, do not include palette with each frame #5603
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Keep transparency when converting from P to LA or PA #5606
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Copy palette to new image in transform() #5647
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Added "transparency" argument to EpsImagePlugin load() #5620
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Corrected pathlib.Path detection when saving #5633
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Added WalImageFile class #5618
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Consider I;16 pixel size when drawing text #5598
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- If default conversion from P is RGB with transparency, convert to RGBA #5594
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Speed up rotating square images by 90 or 270 degrees #5646
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Add support for reading DPI information from JPEG2000 images
|
||||||
|
[rogermb, radarhere]
|
||||||
|
|
||||||
|
- Catch TypeError from corrupted DPI value in EXIF #5639
|
||||||
|
[homm, radarhere]
|
||||||
|
|
||||||
|
- Do not close file pointer when saving SGI images #5645
|
||||||
|
[farizrahman4u, radarhere]
|
||||||
|
|
||||||
|
- Deprecate ImagePalette size parameter #5641
|
||||||
|
[radarhere, hugovk]
|
||||||
|
|
||||||
|
- Prefer command line tools SDK on macOS #5624
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Added tags when saving YCbCr TIFF #5597
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- PSD layer count may be negative #5613
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Fixed ImageOps expand with tuple border on P image #5615
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Fixed error saving APNG with duplicate frames and different duration times #5609
|
||||||
|
[thak1411, radarhere]
|
||||||
|
|
||||||
|
8.3.2 (2021-09-02)
|
||||||
|
------------------
|
||||||
|
|
||||||
|
- CVE-2021-23437 Raise ValueError if color specifier is too long
|
||||||
|
[hugovk, radarhere]
|
||||||
|
|
||||||
|
- Fix 6-byte OOB read in FliDecode
|
||||||
|
[wiredfool]
|
||||||
|
|
||||||
|
- Add support for Python 3.10 #5569, #5570
|
||||||
|
[hugovk, radarhere]
|
||||||
|
|
||||||
|
- Ensure TIFF ``RowsPerStrip`` is multiple of 8 for JPEG compression #5588
|
||||||
|
[kmilos, radarhere]
|
||||||
|
|
||||||
|
- Updates for ``ImagePalette`` channel order #5599
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Hide FriBiDi shim symbols to avoid conflict with real FriBiDi library #5651
|
||||||
|
[nulano]
|
||||||
|
|
||||||
|
8.3.1 (2021-07-06)
|
||||||
|
------------------
|
||||||
|
|
||||||
|
- Catch OSError when checking if fp is sys.stdout #5585
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Handle removing orientation from alternate types of EXIF data #5584
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Make Image.__array__ take optional dtype argument #5572
|
||||||
|
[t-vi, radarhere]
|
||||||
|
|
||||||
8.3.0 (2021-07-01)
|
8.3.0 (2021-07-01)
|
||||||
------------------
|
------------------
|
||||||
|
|
||||||
|
|
|
@ -33,7 +33,7 @@ def _write_png(tmp_path, xdim, ydim):
|
||||||
|
|
||||||
|
|
||||||
def test_large(tmp_path):
|
def test_large(tmp_path):
|
||||||
""" succeeded prepatch"""
|
"""succeeded prepatch"""
|
||||||
_write_png(tmp_path, XDIM, YDIM)
|
_write_png(tmp_path, XDIM, YDIM)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -31,7 +31,7 @@ def _write_png(tmp_path, xdim, ydim):
|
||||||
|
|
||||||
|
|
||||||
def test_large(tmp_path):
|
def test_large(tmp_path):
|
||||||
""" succeeded prepatch"""
|
"""succeeded prepatch"""
|
||||||
_write_png(tmp_path, XDIM, YDIM)
|
_write_png(tmp_path, XDIM, YDIM)
|
||||||
|
|
||||||
|
|
||||||
|
|
BIN
Tests/images/balloon_eciRGBv2_aware.jp2
Normal file
BIN
Tests/images/broken_exif_dpi.jpg
Normal file
After Width: | Height: | Size: 7.6 KiB |
BIN
Tests/images/crash-5762152299364352.fli
Normal file
BIN
Tests/images/exif_imagemagick_orientation.png
Normal file
After Width: | Height: | Size: 175 KiB |
BIN
Tests/images/expected_to_read.jp2
Normal file
BIN
Tests/images/hopper_mask.ico
Normal file
After Width: | Height: | Size: 262 B |
BIN
Tests/images/hopper_mask.png
Normal file
After Width: | Height: | Size: 208 B |
BIN
Tests/images/hopper_wal.png
Normal file
After Width: | Height: | Size: 6.3 KiB |
BIN
Tests/images/invalid_header_length.jp2
Normal file
BIN
Tests/images/negative_layer_count.psd
Normal file
BIN
Tests/images/not_enough_data.jp2
Normal file
BIN
Tests/images/palette_negative.png
Normal file
After Width: | Height: | Size: 17 KiB |
BIN
Tests/images/palette_sepia.png
Normal file
After Width: | Height: | Size: 20 KiB |
BIN
Tests/images/palette_wedge.png
Normal file
After Width: | Height: | Size: 17 KiB |
BIN
Tests/images/reqd_showpage_transparency.png
Normal file
After Width: | Height: | Size: 17 KiB |
BIN
Tests/images/zero_dpi.jp2
Normal file
|
@ -433,12 +433,20 @@ def test_apng_save_duration_loop(tmp_path):
|
||||||
|
|
||||||
# test removal of duplicated frames
|
# test removal of duplicated frames
|
||||||
frame = Image.new("RGBA", (128, 64), (255, 0, 0, 255))
|
frame = Image.new("RGBA", (128, 64), (255, 0, 0, 255))
|
||||||
frame.save(test_file, save_all=True, append_images=[frame], duration=[500, 250])
|
frame.save(
|
||||||
|
test_file, save_all=True, append_images=[frame, frame], duration=[500, 100, 150]
|
||||||
|
)
|
||||||
with Image.open(test_file) as im:
|
with Image.open(test_file) as im:
|
||||||
im.load()
|
im.load()
|
||||||
assert im.n_frames == 1
|
assert im.n_frames == 1
|
||||||
assert im.info.get("duration") == 750
|
assert im.info.get("duration") == 750
|
||||||
|
|
||||||
|
# test info duration
|
||||||
|
frame.info["duration"] = 750
|
||||||
|
frame.save(test_file, save_all=True)
|
||||||
|
with Image.open(test_file) as im:
|
||||||
|
assert im.info.get("duration") == 750
|
||||||
|
|
||||||
|
|
||||||
def test_apng_save_disposal(tmp_path):
|
def test_apng_save_disposal(tmp_path):
|
||||||
test_file = str(tmp_path / "temp.png")
|
test_file = str(tmp_path / "temp.png")
|
||||||
|
@ -529,6 +537,17 @@ def test_apng_save_disposal(tmp_path):
|
||||||
assert im.getpixel((0, 0)) == (0, 255, 0, 255)
|
assert im.getpixel((0, 0)) == (0, 255, 0, 255)
|
||||||
assert im.getpixel((64, 32)) == (0, 255, 0, 255)
|
assert im.getpixel((64, 32)) == (0, 255, 0, 255)
|
||||||
|
|
||||||
|
# test info disposal
|
||||||
|
red.info["disposal"] = PngImagePlugin.APNG_DISPOSE_OP_BACKGROUND
|
||||||
|
red.save(
|
||||||
|
test_file,
|
||||||
|
save_all=True,
|
||||||
|
append_images=[Image.new("RGBA", (10, 10), (0, 255, 0, 255))],
|
||||||
|
)
|
||||||
|
with Image.open(test_file) as im:
|
||||||
|
im.seek(1)
|
||||||
|
assert im.getpixel((64, 32)) == (0, 0, 0, 0)
|
||||||
|
|
||||||
|
|
||||||
def test_apng_save_disposal_previous(tmp_path):
|
def test_apng_save_disposal_previous(tmp_path):
|
||||||
test_file = str(tmp_path / "temp.png")
|
test_file = str(tmp_path / "temp.png")
|
||||||
|
@ -609,3 +628,10 @@ def test_apng_save_blend(tmp_path):
|
||||||
im.seek(2)
|
im.seek(2)
|
||||||
assert im.getpixel((0, 0)) == (0, 255, 0, 255)
|
assert im.getpixel((0, 0)) == (0, 255, 0, 255)
|
||||||
assert im.getpixel((64, 32)) == (0, 255, 0, 255)
|
assert im.getpixel((64, 32)) == (0, 255, 0, 255)
|
||||||
|
|
||||||
|
# test info blend
|
||||||
|
red.info["blend"] = PngImagePlugin.APNG_BLEND_OP_OVER
|
||||||
|
red.save(test_file, save_all=True, append_images=[green, transparent])
|
||||||
|
with Image.open(test_file) as im:
|
||||||
|
im.seek(2)
|
||||||
|
assert im.getpixel((0, 0)) == (0, 255, 0, 255)
|
||||||
|
|
|
@ -197,7 +197,7 @@ def test__accept_false():
|
||||||
|
|
||||||
|
|
||||||
def test_short_header():
|
def test_short_header():
|
||||||
""" Check a short header"""
|
"""Check a short header"""
|
||||||
with open(TEST_FILE_DXT5, "rb") as f:
|
with open(TEST_FILE_DXT5, "rb") as f:
|
||||||
img_file = f.read()
|
img_file = f.read()
|
||||||
|
|
||||||
|
@ -210,7 +210,7 @@ def test_short_header():
|
||||||
|
|
||||||
|
|
||||||
def test_short_file():
|
def test_short_file():
|
||||||
""" Check that the appropriate error is thrown for a short file"""
|
"""Check that the appropriate error is thrown for a short file"""
|
||||||
|
|
||||||
with open(TEST_FILE_DXT5, "rb") as f:
|
with open(TEST_FILE_DXT5, "rb") as f:
|
||||||
img_file = f.read()
|
img_file = f.read()
|
||||||
|
@ -224,7 +224,7 @@ def test_short_file():
|
||||||
|
|
||||||
|
|
||||||
def test_dxt5_colorblock_alpha_issue_4142():
|
def test_dxt5_colorblock_alpha_issue_4142():
|
||||||
""" Check that colorblocks are decoded correctly in DXT5"""
|
"""Check that colorblocks are decoded correctly in DXT5"""
|
||||||
|
|
||||||
with Image.open("Tests/images/dxt5-colorblock-alpha-issue-4142.dds") as im:
|
with Image.open("Tests/images/dxt5-colorblock-alpha-issue-4142.dds") as im:
|
||||||
px = im.getpixel((0, 0))
|
px = im.getpixel((0, 0))
|
||||||
|
|
|
@ -96,6 +96,17 @@ def test_showpage():
|
||||||
assert_image_similar(plot_image, target, 6)
|
assert_image_similar(plot_image, target, 6)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available")
|
||||||
|
def test_transparency():
|
||||||
|
with Image.open("Tests/images/reqd_showpage.eps") as plot_image:
|
||||||
|
plot_image.load(transparency=True)
|
||||||
|
assert plot_image.mode == "RGBA"
|
||||||
|
|
||||||
|
with Image.open("Tests/images/reqd_showpage_transparency.png") as target:
|
||||||
|
# fonts could be slightly different
|
||||||
|
assert_image_similar(plot_image, target, 6)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available")
|
@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available")
|
||||||
def test_file_object(tmp_path):
|
def test_file_object(tmp_path):
|
||||||
# issue 479
|
# issue 479
|
||||||
|
|
|
@ -138,3 +138,16 @@ def test_timeouts(test_file):
|
||||||
with Image.open(f) as im:
|
with Image.open(f) as im:
|
||||||
with pytest.raises(OSError):
|
with pytest.raises(OSError):
|
||||||
im.load()
|
im.load()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"test_file",
|
||||||
|
[
|
||||||
|
"Tests/images/crash-5762152299364352.fli",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_crash(test_file):
|
||||||
|
with open(test_file, "rb") as f:
|
||||||
|
with Image.open(f) as im:
|
||||||
|
with pytest.raises(OSError):
|
||||||
|
im.load()
|
||||||
|
|
|
@ -821,6 +821,29 @@ def test_palette_save_P(tmp_path):
|
||||||
assert_image_equal(reloaded, im)
|
assert_image_equal(reloaded, im)
|
||||||
|
|
||||||
|
|
||||||
|
def test_palette_save_all_P(tmp_path):
|
||||||
|
frames = []
|
||||||
|
colors = ((255, 0, 0), (0, 255, 0))
|
||||||
|
for color in colors:
|
||||||
|
frame = Image.new("P", (100, 100))
|
||||||
|
frame.putpalette(color)
|
||||||
|
frames.append(frame)
|
||||||
|
|
||||||
|
out = str(tmp_path / "temp.gif")
|
||||||
|
frames[0].save(
|
||||||
|
out, save_all=True, palette=[255, 0, 0, 0, 255, 0], append_images=frames[1:]
|
||||||
|
)
|
||||||
|
|
||||||
|
with Image.open(out) as im:
|
||||||
|
# Assert that the frames are correct, and each frame has the same palette
|
||||||
|
assert_image_equal(im.convert("RGB"), frames[0].convert("RGB"))
|
||||||
|
assert im.palette.palette == im.global_palette.palette
|
||||||
|
|
||||||
|
im.seek(1)
|
||||||
|
assert_image_equal(im.convert("RGB"), frames[1].convert("RGB"))
|
||||||
|
assert im.palette.palette == im.global_palette.palette
|
||||||
|
|
||||||
|
|
||||||
def test_palette_save_ImagePalette(tmp_path):
|
def test_palette_save_ImagePalette(tmp_path):
|
||||||
# Pass in a different palette, as an ImagePalette.ImagePalette
|
# Pass in a different palette, as an ImagePalette.ImagePalette
|
||||||
# effectively the same as test_palette_save_P
|
# effectively the same as test_palette_save_P
|
||||||
|
@ -833,7 +856,7 @@ def test_palette_save_ImagePalette(tmp_path):
|
||||||
|
|
||||||
with Image.open(out) as reloaded:
|
with Image.open(out) as reloaded:
|
||||||
im.putpalette(palette)
|
im.putpalette(palette)
|
||||||
assert_image_equal(reloaded, im)
|
assert_image_equal(reloaded.convert("RGB"), im.convert("RGB"))
|
||||||
|
|
||||||
|
|
||||||
def test_save_I(tmp_path):
|
def test_save_I(tmp_path):
|
||||||
|
|
|
@ -18,6 +18,11 @@ def test_sanity():
|
||||||
assert im.get_format_mimetype() == "image/x-icon"
|
assert im.get_format_mimetype() == "image/x-icon"
|
||||||
|
|
||||||
|
|
||||||
|
def test_mask():
|
||||||
|
with Image.open("Tests/images/hopper_mask.ico") as im:
|
||||||
|
assert_image_equal_tofile(im, "Tests/images/hopper_mask.png")
|
||||||
|
|
||||||
|
|
||||||
def test_black_and_white():
|
def test_black_and_white():
|
||||||
with Image.open("Tests/images/black_and_white.ico") as im:
|
with Image.open("Tests/images/black_and_white.ico") as im:
|
||||||
assert im.mode == "RGBA"
|
assert im.mode == "RGBA"
|
||||||
|
|
|
@ -630,7 +630,7 @@ class TestFileJpeg:
|
||||||
reloaded.save(f, quality="keep", optimize=True)
|
reloaded.save(f, quality="keep", optimize=True)
|
||||||
|
|
||||||
def test_bad_mpo_header(self):
|
def test_bad_mpo_header(self):
|
||||||
""" Treat unknown MPO as JPEG """
|
"""Treat unknown MPO as JPEG"""
|
||||||
# Arrange
|
# Arrange
|
||||||
|
|
||||||
# Act
|
# Act
|
||||||
|
@ -718,6 +718,15 @@ class TestFileJpeg:
|
||||||
# This should return the default, and not raise a ZeroDivisionError
|
# This should return the default, and not raise a ZeroDivisionError
|
||||||
assert im.info.get("dpi") == (72, 72)
|
assert im.info.get("dpi") == (72, 72)
|
||||||
|
|
||||||
|
def test_dpi_exif_string(self):
|
||||||
|
# Arrange
|
||||||
|
# 0x011A tag in this exif contains string '300300\x02'
|
||||||
|
with Image.open("Tests/images/broken_exif_dpi.jpg") as im:
|
||||||
|
|
||||||
|
# Act / Assert
|
||||||
|
# This should return the default
|
||||||
|
assert im.info.get("dpi") == (72, 72)
|
||||||
|
|
||||||
def test_no_dpi_in_exif(self):
|
def test_no_dpi_in_exif(self):
|
||||||
# Arrange
|
# Arrange
|
||||||
# This is photoshop-200dpi.jpg with resolution removed from EXIF:
|
# This is photoshop-200dpi.jpg with resolution removed from EXIF:
|
||||||
|
|
|
@ -4,7 +4,7 @@ from io import BytesIO
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from PIL import Image, ImageFile, Jpeg2KImagePlugin, features
|
from PIL import Image, ImageFile, Jpeg2KImagePlugin, UnidentifiedImageError, features
|
||||||
|
|
||||||
from .helper import (
|
from .helper import (
|
||||||
assert_image_equal,
|
assert_image_equal,
|
||||||
|
@ -151,6 +151,38 @@ def test_reduce():
|
||||||
assert im.size == (40, 30)
|
assert im.size == (40, 30)
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_dpi():
|
||||||
|
with Image.open("Tests/images/test-card-lossless.jp2") as im:
|
||||||
|
assert im.info["dpi"] == (71.9836, 71.9836)
|
||||||
|
|
||||||
|
with Image.open("Tests/images/zero_dpi.jp2") as im:
|
||||||
|
assert "dpi" not in im.info
|
||||||
|
|
||||||
|
|
||||||
|
def test_restricted_icc_profile():
|
||||||
|
ImageFile.LOAD_TRUNCATED_IMAGES = True
|
||||||
|
try:
|
||||||
|
# JPEG2000 image with a restricted ICC profile and a known colorspace
|
||||||
|
with Image.open("Tests/images/balloon_eciRGBv2_aware.jp2") as im:
|
||||||
|
assert im.mode == "RGB"
|
||||||
|
finally:
|
||||||
|
ImageFile.LOAD_TRUNCATED_IMAGES = False
|
||||||
|
|
||||||
|
|
||||||
|
def test_header_errors():
|
||||||
|
for path in (
|
||||||
|
"Tests/images/invalid_header_length.jp2",
|
||||||
|
"Tests/images/not_enough_data.jp2",
|
||||||
|
):
|
||||||
|
with pytest.raises(UnidentifiedImageError):
|
||||||
|
with Image.open(path):
|
||||||
|
pass
|
||||||
|
|
||||||
|
with pytest.raises(OSError):
|
||||||
|
with Image.open("Tests/images/expected_to_read.jp2"):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
def test_layers_type(tmp_path):
|
def test_layers_type(tmp_path):
|
||||||
outfile = str(tmp_path / "temp_layers.jp2")
|
outfile = str(tmp_path / "temp_layers.jp2")
|
||||||
for quality_layers in [[100, 50, 10], (100, 50, 10), None]:
|
for quality_layers in [[100, 50, 10], (100, 50, 10), None]:
|
||||||
|
|
|
@ -97,13 +97,13 @@ class TestFileLibTiff(LibTiffTestCase):
|
||||||
self._assert_noerr(tmp_path, im)
|
self._assert_noerr(tmp_path, im)
|
||||||
|
|
||||||
def test_g4_eq_png(self):
|
def test_g4_eq_png(self):
|
||||||
""" Checking that we're actually getting the data that we expect"""
|
"""Checking that we're actually getting the data that we expect"""
|
||||||
with Image.open("Tests/images/hopper_bw_500.png") as png:
|
with Image.open("Tests/images/hopper_bw_500.png") as png:
|
||||||
assert_image_equal_tofile(png, "Tests/images/hopper_g4_500.tif")
|
assert_image_equal_tofile(png, "Tests/images/hopper_g4_500.tif")
|
||||||
|
|
||||||
# see https://github.com/python-pillow/Pillow/issues/279
|
# see https://github.com/python-pillow/Pillow/issues/279
|
||||||
def test_g4_fillorder_eq_png(self):
|
def test_g4_fillorder_eq_png(self):
|
||||||
""" Checking that we're actually getting the data that we expect"""
|
"""Checking that we're actually getting the data that we expect"""
|
||||||
with Image.open("Tests/images/g4-fillorder-test.tif") as g4:
|
with Image.open("Tests/images/g4-fillorder-test.tif") as g4:
|
||||||
assert_image_equal_tofile(g4, "Tests/images/g4-fillorder-test.png")
|
assert_image_equal_tofile(g4, "Tests/images/g4-fillorder-test.png")
|
||||||
|
|
||||||
|
@ -137,7 +137,7 @@ class TestFileLibTiff(LibTiffTestCase):
|
||||||
assert_image_equal_tofile(im, "Tests/images/tiff_adobe_deflate.png")
|
assert_image_equal_tofile(im, "Tests/images/tiff_adobe_deflate.png")
|
||||||
|
|
||||||
def test_write_metadata(self, tmp_path):
|
def test_write_metadata(self, tmp_path):
|
||||||
""" Test metadata writing through libtiff """
|
"""Test metadata writing through libtiff"""
|
||||||
for legacy_api in [False, True]:
|
for legacy_api in [False, True]:
|
||||||
f = str(tmp_path / "temp.tiff")
|
f = str(tmp_path / "temp.tiff")
|
||||||
with Image.open("Tests/images/hopper_g4.tif") as img:
|
with Image.open("Tests/images/hopper_g4.tif") as img:
|
||||||
|
@ -670,6 +670,15 @@ class TestFileLibTiff(LibTiffTestCase):
|
||||||
TiffImagePlugin.WRITE_LIBTIFF = False
|
TiffImagePlugin.WRITE_LIBTIFF = False
|
||||||
TiffImagePlugin.READ_LIBTIFF = False
|
TiffImagePlugin.READ_LIBTIFF = False
|
||||||
|
|
||||||
|
def test_save_ycbcr(self, tmp_path):
|
||||||
|
im = hopper("YCbCr")
|
||||||
|
outfile = str(tmp_path / "temp.tif")
|
||||||
|
im.save(outfile, compression="jpeg")
|
||||||
|
|
||||||
|
with Image.open(outfile) as reloaded:
|
||||||
|
assert reloaded.tag_v2[530] == (1, 1)
|
||||||
|
assert reloaded.tag_v2[532] == (0, 255, 128, 255, 128, 255)
|
||||||
|
|
||||||
def test_crashing_metadata(self, tmp_path):
|
def test_crashing_metadata(self, tmp_path):
|
||||||
# issue 1597
|
# issue 1597
|
||||||
with Image.open("Tests/images/rdf.tif") as im:
|
with Image.open("Tests/images/rdf.tif") as im:
|
||||||
|
@ -968,10 +977,11 @@ class TestFileLibTiff(LibTiffTestCase):
|
||||||
assert str(e.value) == "-9"
|
assert str(e.value) == "-9"
|
||||||
TiffImagePlugin.READ_LIBTIFF = False
|
TiffImagePlugin.READ_LIBTIFF = False
|
||||||
|
|
||||||
def test_save_multistrip(self, tmp_path):
|
@pytest.mark.parametrize("compression", ("tiff_adobe_deflate", "jpeg"))
|
||||||
|
def test_save_multistrip(self, compression, tmp_path):
|
||||||
im = hopper("RGB").resize((256, 256))
|
im = hopper("RGB").resize((256, 256))
|
||||||
out = str(tmp_path / "temp.tif")
|
out = str(tmp_path / "temp.tif")
|
||||||
im.save(out, compression="tiff_adobe_deflate")
|
im.save(out, compression=compression)
|
||||||
|
|
||||||
with Image.open(out) as im:
|
with Image.open(out) as im:
|
||||||
# Assert that there are multiple strips
|
# Assert that there are multiple strips
|
||||||
|
|
|
@ -57,7 +57,8 @@ def test_n_frames():
|
||||||
assert im.n_frames == 1
|
assert im.n_frames == 1
|
||||||
assert not im.is_animated
|
assert not im.is_animated
|
||||||
|
|
||||||
with Image.open(test_file) as im:
|
for path in [test_file, "Tests/images/negative_layer_count.psd"]:
|
||||||
|
with Image.open(path) as im:
|
||||||
assert im.n_frames == 2
|
assert im.n_frames == 2
|
||||||
assert im.is_animated
|
assert im.is_animated
|
||||||
|
|
||||||
|
|
|
@ -73,6 +73,13 @@ def test_write(tmp_path):
|
||||||
img.save(out, format="sgi")
|
img.save(out, format="sgi")
|
||||||
assert_image_equal_tofile(img, out)
|
assert_image_equal_tofile(img, out)
|
||||||
|
|
||||||
|
out = str(tmp_path / "fp.sgi")
|
||||||
|
with open(out, "wb") as fp:
|
||||||
|
img.save(fp)
|
||||||
|
assert_image_equal_tofile(img, out)
|
||||||
|
|
||||||
|
assert not fp.closed
|
||||||
|
|
||||||
for mode in ("L", "RGB", "RGBA"):
|
for mode in ("L", "RGB", "RGBA"):
|
||||||
roundtrip(hopper(mode))
|
roundtrip(hopper(mode))
|
||||||
|
|
||||||
|
|
|
@ -463,6 +463,15 @@ class TestFileTiff:
|
||||||
im.seek(1)
|
im.seek(1)
|
||||||
assert im.getexif()[273] == (1408, 1907)
|
assert im.getexif()[273] == (1408, 1907)
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("mode", ("1", "L"))
|
||||||
|
def test_photometric(self, mode, tmp_path):
|
||||||
|
filename = str(tmp_path / "temp.tif")
|
||||||
|
im = hopper(mode)
|
||||||
|
im.save(filename, tiffinfo={262: 0})
|
||||||
|
with Image.open(filename) as reloaded:
|
||||||
|
assert reloaded.tag_v2[262] == 0
|
||||||
|
assert_image_equal(im, reloaded)
|
||||||
|
|
||||||
def test_seek(self):
|
def test_seek(self):
|
||||||
filename = "Tests/images/pil136.tiff"
|
filename = "Tests/images/pil136.tiff"
|
||||||
with Image.open(filename) as im:
|
with Image.open(filename) as im:
|
||||||
|
@ -705,6 +714,8 @@ class TestFileTiff:
|
||||||
# Ignore this UserWarning which triggers for four tags:
|
# Ignore this UserWarning which triggers for four tags:
|
||||||
# "Possibly corrupt EXIF data. Expecting to read 50404352 bytes but..."
|
# "Possibly corrupt EXIF data. Expecting to read 50404352 bytes but..."
|
||||||
@pytest.mark.filterwarnings("ignore:Possibly corrupt EXIF data")
|
@pytest.mark.filterwarnings("ignore:Possibly corrupt EXIF data")
|
||||||
|
# Ignore this UserWarning:
|
||||||
|
@pytest.mark.filterwarnings("ignore:Truncated File Read")
|
||||||
@pytest.mark.skipif(
|
@pytest.mark.skipif(
|
||||||
not os.path.exists("Tests/images/string_dimension.tiff"),
|
not os.path.exists("Tests/images/string_dimension.tiff"),
|
||||||
reason="Extra image files not installed",
|
reason="Extra image files not installed",
|
||||||
|
|
|
@ -122,7 +122,7 @@ def test_read_metadata():
|
||||||
|
|
||||||
|
|
||||||
def test_write_metadata(tmp_path):
|
def test_write_metadata(tmp_path):
|
||||||
""" Test metadata writing through the python code """
|
"""Test metadata writing through the python code"""
|
||||||
with Image.open("Tests/images/hopper.tif") as img:
|
with Image.open("Tests/images/hopper.tif") as img:
|
||||||
f = str(tmp_path / "temp.tiff")
|
f = str(tmp_path / "temp.tiff")
|
||||||
img.save(f, tiffinfo=img.tag)
|
img.save(f, tiffinfo=img.tag)
|
||||||
|
|
|
@ -1,15 +1,21 @@
|
||||||
from PIL import WalImageFile
|
from PIL import WalImageFile
|
||||||
|
|
||||||
|
from .helper import assert_image_equal_tofile
|
||||||
|
|
||||||
|
|
||||||
def test_open():
|
def test_open():
|
||||||
# Arrange
|
# Arrange
|
||||||
TEST_FILE = "Tests/images/hopper.wal"
|
TEST_FILE = "Tests/images/hopper.wal"
|
||||||
|
|
||||||
# Act
|
# Act
|
||||||
im = WalImageFile.open(TEST_FILE)
|
with WalImageFile.open(TEST_FILE) as im:
|
||||||
|
|
||||||
# Assert
|
# Assert
|
||||||
assert im.format == "WAL"
|
assert im.format == "WAL"
|
||||||
assert im.format_description == "Quake2 Texture"
|
assert im.format_description == "Quake2 Texture"
|
||||||
assert im.mode == "P"
|
assert im.mode == "P"
|
||||||
assert im.size == (128, 128)
|
assert im.size == (128, 128)
|
||||||
|
|
||||||
|
assert isinstance(im, WalImageFile.WalImageFile)
|
||||||
|
|
||||||
|
assert_image_equal_tofile(im, "Tests/images/hopper_wal.png")
|
||||||
|
|
|
@ -104,6 +104,13 @@ class TestFileWebp:
|
||||||
hopper().save(buffer_method, format="WEBP", method=6)
|
hopper().save(buffer_method, format="WEBP", method=6)
|
||||||
assert buffer_no_args.getbuffer() != buffer_method.getbuffer()
|
assert buffer_no_args.getbuffer() != buffer_method.getbuffer()
|
||||||
|
|
||||||
|
def test_icc_profile(self, tmp_path):
|
||||||
|
self._roundtrip(tmp_path, self.rgb_mode, 12.5, {"icc_profile": None})
|
||||||
|
if _webp.HAVE_WEBPANIM:
|
||||||
|
self._roundtrip(
|
||||||
|
tmp_path, self.rgb_mode, 12.5, {"icc_profile": None, "save_all": True}
|
||||||
|
)
|
||||||
|
|
||||||
def test_write_unsupported_mode_L(self, tmp_path):
|
def test_write_unsupported_mode_L(self, tmp_path):
|
||||||
"""
|
"""
|
||||||
Saving a black-and-white file to WebP format should work, and be
|
Saving a black-and-white file to WebP format should work, and be
|
||||||
|
|
|
@ -149,7 +149,8 @@ class TestImage:
|
||||||
assert im.mode == "RGB"
|
assert im.mode == "RGB"
|
||||||
assert im.size == (128, 128)
|
assert im.size == (128, 128)
|
||||||
|
|
||||||
temp_file = str(tmp_path / "temp.jpg")
|
for ext in (".jpg", ".jp2"):
|
||||||
|
temp_file = str(tmp_path / ("temp." + ext))
|
||||||
if os.path.exists(temp_file):
|
if os.path.exists(temp_file):
|
||||||
os.remove(temp_file)
|
os.remove(temp_file)
|
||||||
im.save(Path(temp_file))
|
im.save(Path(temp_file))
|
||||||
|
|
|
@ -14,6 +14,10 @@ def test_toarray():
|
||||||
ai = numpy.array(im.convert(mode))
|
ai = numpy.array(im.convert(mode))
|
||||||
return ai.shape, ai.dtype.str, ai.nbytes
|
return ai.shape, ai.dtype.str, ai.nbytes
|
||||||
|
|
||||||
|
def test_with_dtype(dtype):
|
||||||
|
ai = numpy.array(im, dtype=dtype)
|
||||||
|
assert ai.dtype == dtype
|
||||||
|
|
||||||
# assert test("1") == ((100, 128), '|b1', 1600))
|
# assert test("1") == ((100, 128), '|b1', 1600))
|
||||||
assert test("L") == ((100, 128), "|u1", 12800)
|
assert test("L") == ((100, 128), "|u1", 12800)
|
||||||
|
|
||||||
|
@ -27,6 +31,9 @@ def test_toarray():
|
||||||
assert test("RGBA") == ((100, 128, 4), "|u1", 51200)
|
assert test("RGBA") == ((100, 128, 4), "|u1", 51200)
|
||||||
assert test("RGBX") == ((100, 128, 4), "|u1", 51200)
|
assert test("RGBX") == ((100, 128, 4), "|u1", 51200)
|
||||||
|
|
||||||
|
test_with_dtype(numpy.float64)
|
||||||
|
test_with_dtype(numpy.uint8)
|
||||||
|
|
||||||
with Image.open("Tests/images/truncated_jpeg.jpg") as im_truncated:
|
with Image.open("Tests/images/truncated_jpeg.jpg") as im_truncated:
|
||||||
with pytest.raises(OSError):
|
with pytest.raises(OSError):
|
||||||
numpy.array(im_truncated)
|
numpy.array(im_truncated)
|
||||||
|
@ -34,7 +41,7 @@ def test_toarray():
|
||||||
|
|
||||||
def test_fromarray():
|
def test_fromarray():
|
||||||
class Wrapper:
|
class Wrapper:
|
||||||
""" Class with API matching Image.fromarray """
|
"""Class with API matching Image.fromarray"""
|
||||||
|
|
||||||
def __init__(self, img, arr_params):
|
def __init__(self, img, arr_params):
|
||||||
self.img = img
|
self.img = img
|
||||||
|
|
|
@ -42,10 +42,14 @@ def test_default():
|
||||||
|
|
||||||
im = hopper("P")
|
im = hopper("P")
|
||||||
assert_image(im, "P", im.size)
|
assert_image(im, "P", im.size)
|
||||||
im = im.convert()
|
converted_im = im.convert()
|
||||||
assert_image(im, "RGB", im.size)
|
assert_image(converted_im, "RGB", im.size)
|
||||||
im = im.convert()
|
converted_im = im.convert()
|
||||||
assert_image(im, "RGB", im.size)
|
assert_image(converted_im, "RGB", im.size)
|
||||||
|
|
||||||
|
im.info["transparency"] = 0
|
||||||
|
converted_im = im.convert()
|
||||||
|
assert_image(converted_im, "RGBA", im.size)
|
||||||
|
|
||||||
|
|
||||||
# ref https://github.com/python-pillow/Pillow/issues/274
|
# ref https://github.com/python-pillow/Pillow/issues/274
|
||||||
|
@ -100,18 +104,22 @@ def test_trns_p(tmp_path):
|
||||||
# ref https://github.com/python-pillow/Pillow/issues/664
|
# ref https://github.com/python-pillow/Pillow/issues/664
|
||||||
|
|
||||||
|
|
||||||
def test_trns_p_rgba():
|
@pytest.mark.parametrize("mode", ("LA", "PA", "RGBA"))
|
||||||
|
def test_trns_p_transparency(mode):
|
||||||
# Arrange
|
# Arrange
|
||||||
im = hopper("P")
|
im = hopper("P")
|
||||||
im.info["transparency"] = 128
|
im.info["transparency"] = 128
|
||||||
|
|
||||||
# Act
|
# Act
|
||||||
im_rgba = im.convert("RGBA")
|
converted_im = im.convert(mode)
|
||||||
|
|
||||||
# Assert
|
# Assert
|
||||||
assert "transparency" not in im_rgba.info
|
assert "transparency" not in converted_im.info
|
||||||
|
if mode == "PA":
|
||||||
|
assert converted_im.palette is not None
|
||||||
|
else:
|
||||||
# https://github.com/python-pillow/Pillow/issues/2702
|
# https://github.com/python-pillow/Pillow/issues/2702
|
||||||
assert im_rgba.palette is None
|
assert converted_im.palette is None
|
||||||
|
|
||||||
|
|
||||||
def test_trns_l(tmp_path):
|
def test_trns_l(tmp_path):
|
||||||
|
|
|
@ -32,7 +32,7 @@ def test_16bit_lut():
|
||||||
|
|
||||||
|
|
||||||
def test_f_lut():
|
def test_f_lut():
|
||||||
""" Tests for floating point lut of 8bit gray image """
|
"""Tests for floating point lut of 8bit gray image"""
|
||||||
im = hopper("L")
|
im = hopper("L")
|
||||||
lut = [0.5 * float(x) for x in range(256)]
|
lut = [0.5 * float(x) for x in range(256)]
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@ import pytest
|
||||||
|
|
||||||
from PIL import Image, ImagePalette
|
from PIL import Image, ImagePalette
|
||||||
|
|
||||||
from .helper import assert_image_equal, hopper
|
from .helper import assert_image_equal, assert_image_equal_tofile, hopper
|
||||||
|
|
||||||
|
|
||||||
def test_putpalette():
|
def test_putpalette():
|
||||||
|
@ -36,9 +36,15 @@ def test_putpalette():
|
||||||
def test_imagepalette():
|
def test_imagepalette():
|
||||||
im = hopper("P")
|
im = hopper("P")
|
||||||
im.putpalette(ImagePalette.negative())
|
im.putpalette(ImagePalette.negative())
|
||||||
|
assert_image_equal_tofile(im.convert("RGB"), "Tests/images/palette_negative.png")
|
||||||
|
|
||||||
im.putpalette(ImagePalette.random())
|
im.putpalette(ImagePalette.random())
|
||||||
|
|
||||||
im.putpalette(ImagePalette.sepia())
|
im.putpalette(ImagePalette.sepia())
|
||||||
|
assert_image_equal_tofile(im.convert("RGB"), "Tests/images/palette_sepia.png")
|
||||||
|
|
||||||
im.putpalette(ImagePalette.wedge())
|
im.putpalette(ImagePalette.wedge())
|
||||||
|
assert_image_equal_tofile(im.convert("RGB"), "Tests/images/palette_wedge.png")
|
||||||
|
|
||||||
|
|
||||||
def test_putpalette_with_alpha_values():
|
def test_putpalette_with_alpha_values():
|
||||||
|
|
|
@ -63,6 +63,7 @@ def test_quantize_no_dither():
|
||||||
|
|
||||||
converted = image.quantize(dither=0, palette=palette)
|
converted = image.quantize(dither=0, palette=palette)
|
||||||
assert_image(converted, "P", converted.size)
|
assert_image(converted, "P", converted.size)
|
||||||
|
assert converted.palette.palette == palette.palette.palette
|
||||||
|
|
||||||
|
|
||||||
def test_quantize_dither_diff():
|
def test_quantize_dither_diff():
|
||||||
|
|
|
@ -33,6 +33,9 @@ def test_angle():
|
||||||
with Image.open("Tests/images/test-card.png") as im:
|
with Image.open("Tests/images/test-card.png") as im:
|
||||||
rotate(im, im.mode, angle)
|
rotate(im, im.mode, angle)
|
||||||
|
|
||||||
|
im = hopper()
|
||||||
|
assert_image_equal(im.rotate(angle), im.rotate(angle, expand=1))
|
||||||
|
|
||||||
|
|
||||||
def test_zero():
|
def test_zero():
|
||||||
for angle in (0, 45, 90, 180, 270):
|
for angle in (0, 45, 90, 180, 270):
|
||||||
|
|
|
@ -32,6 +32,11 @@ class TestImageTransform:
|
||||||
new_im = im.transform((100, 100), transform)
|
new_im = im.transform((100, 100), transform)
|
||||||
assert new_im.info["comment"] == comment
|
assert new_im.info["comment"] == comment
|
||||||
|
|
||||||
|
def test_palette(self):
|
||||||
|
with Image.open("Tests/images/hopper.gif") as im:
|
||||||
|
transformed = im.transform(im.size, Image.AFFINE, [1, 0, 0, 0, 1, 0])
|
||||||
|
assert im.palette.palette == transformed.palette.palette
|
||||||
|
|
||||||
def test_extent(self):
|
def test_extent(self):
|
||||||
im = hopper("RGB")
|
im = hopper("RGB")
|
||||||
(w, h) = im.size
|
(w, h) = im.size
|
||||||
|
|
|
@ -191,3 +191,12 @@ def test_rounding_errors():
|
||||||
assert (255, 255) == ImageColor.getcolor("white", "LA")
|
assert (255, 255) == ImageColor.getcolor("white", "LA")
|
||||||
assert (163, 33) == ImageColor.getcolor("rgba(0, 255, 115, 33)", "LA")
|
assert (163, 33) == ImageColor.getcolor("rgba(0, 255, 115, 33)", "LA")
|
||||||
Image.new("LA", (1, 1), "white")
|
Image.new("LA", (1, 1), "white")
|
||||||
|
|
||||||
|
|
||||||
|
def test_color_too_long():
|
||||||
|
# Arrange
|
||||||
|
color_too_long = "hsl(" + "1" * 40 + "," + "1" * 40 + "%," + "1" * 40 + "%)"
|
||||||
|
|
||||||
|
# Act / Assert
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
ImageColor.getrgb(color_too_long)
|
||||||
|
|
|
@ -134,6 +134,17 @@ class TestImageFont:
|
||||||
target = "Tests/images/transparent_background_text_L.png"
|
target = "Tests/images/transparent_background_text_L.png"
|
||||||
assert_image_similar_tofile(im.convert("L"), target, 0.01)
|
assert_image_similar_tofile(im.convert("L"), target, 0.01)
|
||||||
|
|
||||||
|
def test_I16(self):
|
||||||
|
im = Image.new(mode="I;16", size=(300, 100))
|
||||||
|
draw = ImageDraw.Draw(im)
|
||||||
|
ttf = self.get_font()
|
||||||
|
|
||||||
|
txt = "Hello World!"
|
||||||
|
draw.text((10, 10), txt, font=ttf)
|
||||||
|
|
||||||
|
target = "Tests/images/transparent_background_text_L.png"
|
||||||
|
assert_image_similar_tofile(im.convert("L"), target, 0.01)
|
||||||
|
|
||||||
def test_textsize_equal(self):
|
def test_textsize_equal(self):
|
||||||
im = Image.new(mode="RGB", size=(300, 100))
|
im = Image.new(mode="RGB", size=(300, 100))
|
||||||
draw = ImageDraw.Draw(im)
|
draw = ImageDraw.Draw(im)
|
||||||
|
|
|
@ -156,21 +156,29 @@ def test_scale():
|
||||||
assert newimg.size == (25, 25)
|
assert newimg.size == (25, 25)
|
||||||
|
|
||||||
|
|
||||||
def test_expand_palette():
|
@pytest.mark.parametrize("border", (10, (1, 2, 3, 4)))
|
||||||
im = Image.open("Tests/images/p_16.tga")
|
def test_expand_palette(border):
|
||||||
im_expanded = ImageOps.expand(im, 10, (255, 0, 0))
|
with Image.open("Tests/images/p_16.tga") as im:
|
||||||
|
im_expanded = ImageOps.expand(im, border, (255, 0, 0))
|
||||||
|
|
||||||
|
if isinstance(border, int):
|
||||||
|
left = top = right = bottom = border
|
||||||
|
else:
|
||||||
|
left, top, right, bottom = border
|
||||||
px = im_expanded.convert("RGB").load()
|
px = im_expanded.convert("RGB").load()
|
||||||
for b in range(10):
|
|
||||||
for x in range(im_expanded.width):
|
for x in range(im_expanded.width):
|
||||||
|
for b in range(top):
|
||||||
assert px[x, b] == (255, 0, 0)
|
assert px[x, b] == (255, 0, 0)
|
||||||
|
for b in range(bottom):
|
||||||
assert px[x, im_expanded.height - 1 - b] == (255, 0, 0)
|
assert px[x, im_expanded.height - 1 - b] == (255, 0, 0)
|
||||||
for y in range(im_expanded.height):
|
for y in range(im_expanded.height):
|
||||||
assert px[b, x] == (255, 0, 0)
|
for b in range(left):
|
||||||
assert px[b, im_expanded.width - 1 - b] == (255, 0, 0)
|
assert px[b, y] == (255, 0, 0)
|
||||||
|
for b in range(right):
|
||||||
|
assert px[im_expanded.width - 1 - b, y] == (255, 0, 0)
|
||||||
|
|
||||||
im_cropped = im_expanded.crop(
|
im_cropped = im_expanded.crop(
|
||||||
(10, 10, im_expanded.width - 10, im_expanded.height - 10)
|
(left, top, im_expanded.width - right, im_expanded.height - bottom)
|
||||||
)
|
)
|
||||||
assert_image_equal(im_cropped, im)
|
assert_image_equal(im_cropped, im)
|
||||||
|
|
||||||
|
@ -335,6 +343,28 @@ def test_exif_transpose():
|
||||||
) as orientation_im:
|
) as orientation_im:
|
||||||
check(orientation_im)
|
check(orientation_im)
|
||||||
|
|
||||||
|
# Orientation from "XML:com.adobe.xmp" info key
|
||||||
|
with Image.open("Tests/images/xmp_tags_orientation.png") as im:
|
||||||
|
assert im.getexif()[0x0112] == 3
|
||||||
|
|
||||||
|
transposed_im = ImageOps.exif_transpose(im)
|
||||||
|
assert 0x0112 not in transposed_im.getexif()
|
||||||
|
|
||||||
|
# Orientation from "Raw profile type exif" info key
|
||||||
|
# This test image has been manually hexedited from exif_imagemagick.png
|
||||||
|
# to have a different orientation
|
||||||
|
with Image.open("Tests/images/exif_imagemagick_orientation.png") as im:
|
||||||
|
assert im.getexif()[0x0112] == 3
|
||||||
|
|
||||||
|
transposed_im = ImageOps.exif_transpose(im)
|
||||||
|
assert 0x0112 not in transposed_im.getexif()
|
||||||
|
|
||||||
|
# Orientation set directly on Image.Exif
|
||||||
|
im = hopper()
|
||||||
|
im.getexif()[0x0112] = 3
|
||||||
|
transposed_im = ImageOps.exif_transpose(im)
|
||||||
|
assert 0x0112 not in transposed_im.getexif()
|
||||||
|
|
||||||
|
|
||||||
def test_autocontrast_cutoff():
|
def test_autocontrast_cutoff():
|
||||||
# Test the cutoff argument of autocontrast
|
# Test the cutoff argument of autocontrast
|
||||||
|
|
|
@ -10,12 +10,13 @@ def test_sanity():
|
||||||
palette = ImagePalette.ImagePalette("RGB", list(range(256)) * 3)
|
palette = ImagePalette.ImagePalette("RGB", list(range(256)) * 3)
|
||||||
assert len(palette.colors) == 256
|
assert len(palette.colors) == 256
|
||||||
|
|
||||||
|
with pytest.warns(DeprecationWarning):
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
ImagePalette.ImagePalette("RGB", list(range(256)) * 3, 10)
|
ImagePalette.ImagePalette("RGB", list(range(256)) * 3, 10)
|
||||||
|
|
||||||
|
|
||||||
def test_reload():
|
def test_reload():
|
||||||
im = Image.open("Tests/images/hopper.gif")
|
with Image.open("Tests/images/hopper.gif") as im:
|
||||||
original = im.copy()
|
original = im.copy()
|
||||||
im.palette.dirty = 1
|
im.palette.dirty = 1
|
||||||
assert_image_equal(im.convert("RGB"), original.convert("RGB"))
|
assert_image_equal(im.convert("RGB"), original.convert("RGB"))
|
||||||
|
|
|
@ -24,11 +24,17 @@ def test_overflow():
|
||||||
|
|
||||||
|
|
||||||
def test_tobytes():
|
def test_tobytes():
|
||||||
|
# Note that this image triggers the decompression bomb warning:
|
||||||
|
max_pixels = Image.MAX_IMAGE_PIXELS
|
||||||
|
Image.MAX_IMAGE_PIXELS = None
|
||||||
|
|
||||||
# Previously raised an access violation on Windows
|
# Previously raised an access violation on Windows
|
||||||
with Image.open("Tests/images/l2rgb_read.bmp") as im:
|
with Image.open("Tests/images/l2rgb_read.bmp") as im:
|
||||||
with pytest.raises((ValueError, MemoryError, OSError)):
|
with pytest.raises((ValueError, MemoryError, OSError)):
|
||||||
im.tobytes()
|
im.tobytes()
|
||||||
|
|
||||||
|
Image.MAX_IMAGE_PIXELS = max_pixels
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.skipif(sys.maxsize <= 2 ** 32, reason="Requires 64-bit system")
|
@pytest.mark.skipif(sys.maxsize <= 2 ** 32, reason="Requires 64-bit system")
|
||||||
def test_ysize():
|
def test_ysize():
|
||||||
|
|
|
@ -40,6 +40,7 @@ from .helper import on_ci
|
||||||
)
|
)
|
||||||
@pytest.mark.filterwarnings("ignore:Possibly corrupt EXIF data")
|
@pytest.mark.filterwarnings("ignore:Possibly corrupt EXIF data")
|
||||||
@pytest.mark.filterwarnings("ignore:Metadata warning")
|
@pytest.mark.filterwarnings("ignore:Metadata warning")
|
||||||
|
@pytest.mark.filterwarnings("ignore:Truncated File Read")
|
||||||
def test_tiff_crashes(test_file):
|
def test_tiff_crashes(test_file):
|
||||||
try:
|
try:
|
||||||
with Image.open(test_file) as im:
|
with Image.open(test_file) as im:
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# install webp
|
# install webp
|
||||||
|
|
||||||
archive=libwebp-1.2.0
|
archive=libwebp-1.2.1
|
||||||
|
|
||||||
./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/master/$archive.tar.gz
|
./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/master/$archive.tar.gz
|
||||||
|
|
||||||
|
|
|
@ -92,6 +92,17 @@ dictionary. The :py:attr:`~PIL.JpegImagePlugin.convert_dict_qtables` method no l
|
||||||
performs any operations on the data given to it, has been deprecated and will be
|
performs any operations on the data given to it, has been deprecated and will be
|
||||||
removed in Pillow 10.0.0 (2023-01-02).
|
removed in Pillow 10.0.0 (2023-01-02).
|
||||||
|
|
||||||
|
ImagePalette size parameter
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
.. deprecated:: 8.4.0
|
||||||
|
|
||||||
|
The ``size`` parameter will be removed in Pillow 10.0.0 (2023-01-02).
|
||||||
|
|
||||||
|
Before Pillow 8.3.0, ``ImagePalette`` required palette data of particular lengths by
|
||||||
|
default, and the size parameter could be used to override that. Pillow 8.3.0 removed
|
||||||
|
the default required length, also removing the need for the size parameter.
|
||||||
|
|
||||||
Removed features
|
Removed features
|
||||||
----------------
|
----------------
|
||||||
|
|
||||||
|
|
|
@ -66,7 +66,7 @@ than leaving them in the original color space. The EPS driver can write images
|
||||||
in ``L``, ``RGB`` and ``CMYK`` modes.
|
in ``L``, ``RGB`` and ``CMYK`` modes.
|
||||||
|
|
||||||
If Ghostscript is available, you can call the :py:meth:`~PIL.Image.Image.load`
|
If Ghostscript is available, you can call the :py:meth:`~PIL.Image.Image.load`
|
||||||
method with the following parameter to affect how Ghostscript renders the EPS
|
method with the following parameters to affect how Ghostscript renders the EPS
|
||||||
|
|
||||||
**scale**
|
**scale**
|
||||||
Affects the scale of the resultant rasterized image. If the EPS suggests
|
Affects the scale of the resultant rasterized image. If the EPS suggests
|
||||||
|
@ -79,6 +79,11 @@ method with the following parameter to affect how Ghostscript renders the EPS
|
||||||
im.load(scale=2)
|
im.load(scale=2)
|
||||||
im.size #(200,200)
|
im.size #(200,200)
|
||||||
|
|
||||||
|
**transparency**
|
||||||
|
If true, generates an RGBA image with a transparent background, instead of
|
||||||
|
the default behaviour of an RGB image with a white background.
|
||||||
|
|
||||||
|
|
||||||
GIF
|
GIF
|
||||||
^^^
|
^^^
|
||||||
|
|
||||||
|
@ -839,7 +844,7 @@ Reading Multi-frame TIFF Images
|
||||||
The TIFF loader supports the :py:meth:`~PIL.Image.Image.seek` and
|
The TIFF loader supports the :py:meth:`~PIL.Image.Image.seek` and
|
||||||
:py:meth:`~PIL.Image.Image.tell` methods, taking and returning frame numbers
|
:py:meth:`~PIL.Image.Image.tell` methods, taking and returning frame numbers
|
||||||
within the image file. You can combine these methods to seek to the next frame
|
within the image file. You can combine these methods to seek to the next frame
|
||||||
(``im.seek(im.tell() + 1)``). Frames are numbered from 0 to ``im.num_frames - 1``,
|
(``im.seek(im.tell() + 1)``). Frames are numbered from 0 to ``im.n_frames - 1``,
|
||||||
and can be accessed in any order.
|
and can be accessed in any order.
|
||||||
|
|
||||||
``im.seek()`` raises an :py:exc:`EOFError` if you try to seek after the
|
``im.seek()`` raises an :py:exc:`EOFError` if you try to seek after the
|
||||||
|
|
|
@ -18,9 +18,9 @@ Pillow supports these Python versions.
|
||||||
+----------------------+-----+-----+-----+-----+-----+-----+-----+-----+
|
+----------------------+-----+-----+-----+-----+-----+-----+-----+-----+
|
||||||
| Python |3.10 | 3.9 | 3.8 | 3.7 | 3.6 | 3.5 | 3.4 | 2.7 |
|
| Python |3.10 | 3.9 | 3.8 | 3.7 | 3.6 | 3.5 | 3.4 | 2.7 |
|
||||||
+======================+=====+=====+=====+=====+=====+=====+=====+=====+
|
+======================+=====+=====+=====+=====+=====+=====+=====+=====+
|
||||||
| Pillow >= 8.3 | Yes | Yes | Yes | Yes | Yes | | | |
|
| Pillow >= 8.3.2 | Yes | Yes | Yes | Yes | Yes | | | |
|
||||||
+----------------------+-----+-----+-----+-----+-----+-----+-----+-----+
|
+----------------------+-----+-----+-----+-----+-----+-----+-----+-----+
|
||||||
| Pillow 8.0 - 8.2 | | Yes | Yes | Yes | Yes | | | |
|
| Pillow 8.0 - 8.3.1 | | Yes | Yes | Yes | Yes | | | |
|
||||||
+----------------------+-----+-----+-----+-----+-----+-----+-----+-----+
|
+----------------------+-----+-----+-----+-----+-----+-----+-----+-----+
|
||||||
| Pillow 7.0 - 7.2 | | | Yes | Yes | Yes | Yes | | |
|
| Pillow 7.0 - 7.2 | | | Yes | Yes | Yes | Yes | | |
|
||||||
+----------------------+-----+-----+-----+-----+-----+-----+-----+-----+
|
+----------------------+-----+-----+-----+-----+-----+-----+-----+-----+
|
||||||
|
@ -111,7 +111,7 @@ Pillow can be installed on FreeBSD via the official Ports or Packages systems:
|
||||||
|
|
||||||
**Packages**::
|
**Packages**::
|
||||||
|
|
||||||
pkg install py36-pillow
|
pkg install py38-pillow
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
|
|
||||||
|
@ -476,7 +476,7 @@ These platforms are built and tested for every change.
|
||||||
| +---------------------------+---------------------+
|
| +---------------------------+---------------------+
|
||||||
| | PyPy3 | x86 |
|
| | PyPy3 | x86 |
|
||||||
| +---------------------------+---------------------+
|
| +---------------------------+---------------------+
|
||||||
| | 3.8/MinGW | x86, x86-64 |
|
| | 3.9/MinGW | x86, x86-64 |
|
||||||
+----------------------------------+---------------------------+---------------------+
|
+----------------------------------+---------------------------+---------------------+
|
||||||
|
|
||||||
|
|
||||||
|
@ -494,11 +494,11 @@ These platforms have been reported to work at the versions mentioned.
|
||||||
| Operating system | | Tested Python | | Latest tested | | Tested |
|
| Operating system | | Tested Python | | Latest tested | | Tested |
|
||||||
| | | versions | | Pillow version | | processors |
|
| | | versions | | Pillow version | | processors |
|
||||||
+==================================+===========================+==================+==============+
|
+==================================+===========================+==================+==============+
|
||||||
| macOS 11.0 Big Sur | 3.7, 3.8, 3.9 | 8.2.0 |arm |
|
| macOS 11.0 Big Sur | 3.7, 3.8, 3.9 | 8.3.1 |arm |
|
||||||
| +---------------------------+------------------+--------------+
|
| +---------------------------+------------------+--------------+
|
||||||
| | 3.6, 3.7, 3.8, 3.9 | 8.2.0 |x86-64 |
|
| | 3.6, 3.7, 3.8, 3.9 | 8.3.1 |x86-64 |
|
||||||
+----------------------------------+---------------------------+------------------+--------------+
|
+----------------------------------+---------------------------+------------------+--------------+
|
||||||
| macOS 10.15 Catalina | 3.6, 3.7, 3.8, 3.9 | 8.0.1 |x86-64 |
|
| macOS 10.15 Catalina | 3.6, 3.7, 3.8, 3.9 | 8.3.1 |x86-64 |
|
||||||
| +---------------------------+------------------+ |
|
| +---------------------------+------------------+ |
|
||||||
| | 3.5 | 7.2.0 | |
|
| | 3.5 | 7.2.0 | |
|
||||||
+----------------------------------+---------------------------+------------------+--------------+
|
+----------------------------------+---------------------------+------------------+--------------+
|
||||||
|
|
|
@ -9,10 +9,6 @@ represent the color palette of palette mapped images.
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
|
|
||||||
This module was never well-documented. It hasn't changed since 2001,
|
|
||||||
though, so it's probably safe for you to read the source code and puzzle
|
|
||||||
out the internals if you need to.
|
|
||||||
|
|
||||||
The :py:class:`~PIL.ImagePalette.ImagePalette` class has several methods,
|
The :py:class:`~PIL.ImagePalette.ImagePalette` class has several methods,
|
||||||
but they are all marked as "experimental." Read that as you will. The
|
but they are all marked as "experimental." Read that as you will. The
|
||||||
``[source]`` link is there for a reason.
|
``[source]`` link is there for a reason.
|
||||||
|
|
|
@ -339,7 +339,7 @@ Take your test image, and make a really simple harness.
|
||||||
(vpy38-dbg) ubuntu@primary:~/Home/tests$ gdb python
|
(vpy38-dbg) ubuntu@primary:~/Home/tests$ gdb python
|
||||||
GNU gdb (Ubuntu 9.2-0ubuntu1~20.04) 9.2
|
GNU gdb (Ubuntu 9.2-0ubuntu1~20.04) 9.2
|
||||||
Copyright (C) 2020 Free Software Foundation, Inc.
|
Copyright (C) 2020 Free Software Foundation, Inc.
|
||||||
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
|
License GPLv3+: GNU GPL version 3 or later <https://gnu.org/licenses/gpl.html>
|
||||||
This is free software: you are free to change and redistribute it.
|
This is free software: you are free to change and redistribute it.
|
||||||
There is NO WARRANTY, to the extent permitted by law.
|
There is NO WARRANTY, to the extent permitted by law.
|
||||||
Type "show copying" and "show warranty" for details.
|
Type "show copying" and "show warranty" for details.
|
||||||
|
@ -348,7 +348,7 @@ Take your test image, and make a really simple harness.
|
||||||
For bug reporting instructions, please see:
|
For bug reporting instructions, please see:
|
||||||
<http://www.gnu.org/software/gdb/bugs/>.
|
<http://www.gnu.org/software/gdb/bugs/>.
|
||||||
Find the GDB manual and other documentation resources online at:
|
Find the GDB manual and other documentation resources online at:
|
||||||
<http://www.gnu.org/software/gdb/documentation/>.
|
<https://www.gnu.org/software/gdb/documentation/>.
|
||||||
|
|
||||||
For help, type "help".
|
For help, type "help".
|
||||||
Type "apropos word" to search for commands related to "word"...
|
Type "apropos word" to search for commands related to "word"...
|
||||||
|
|
|
@ -14,7 +14,7 @@ Png text chunk size limits
|
||||||
To prevent potential denial of service attacks using compressed text
|
To prevent potential denial of service attacks using compressed text
|
||||||
chunks, there are now limits to the decompressed size of text chunks
|
chunks, there are now limits to the decompressed size of text chunks
|
||||||
decoded from PNG images. If the limits are exceeded when opening a PNG
|
decoded from PNG images. If the limits are exceeded when opening a PNG
|
||||||
image a ``ValueError`` will be raised.
|
image a :py:exc:`ValueError` will be raised.
|
||||||
|
|
||||||
Individual text chunks are limited to
|
Individual text chunks are limited to
|
||||||
:py:attr:`PIL.PngImagePlugin.MAX_TEXT_CHUNK`, set to 1MB by
|
:py:attr:`PIL.PngImagePlugin.MAX_TEXT_CHUNK`, set to 1MB by
|
||||||
|
|
40
docs/releasenotes/8.3.1.rst
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
8.3.1
|
||||||
|
-----
|
||||||
|
|
||||||
|
Fixed regression converting to NumPy arrays
|
||||||
|
===========================================
|
||||||
|
|
||||||
|
This fixes a regression introduced in 8.3.0 when converting an image to a NumPy array
|
||||||
|
with a ``dtype`` argument.
|
||||||
|
|
||||||
|
.. code-block:: pycon
|
||||||
|
|
||||||
|
>>> from PIL import Image
|
||||||
|
>>> import numpy
|
||||||
|
>>> im = Image.new("RGB", (100, 100))
|
||||||
|
>>> numpy.array(im, dtype=numpy.float64)
|
||||||
|
Traceback (most recent call last):
|
||||||
|
File "<stdin>", line 1, in <module>
|
||||||
|
TypeError: __array__() takes 1 positional argument but 2 were given
|
||||||
|
>>>
|
||||||
|
|
||||||
|
Catch OSError when checking if destination is sys.stdout
|
||||||
|
========================================================
|
||||||
|
|
||||||
|
In 8.3.0, a check to see if the destination was ``sys.stdout`` when saving an image was
|
||||||
|
updated. This lead to an OSError being raised if the environment restricted access.
|
||||||
|
|
||||||
|
The OSError is now silently caught.
|
||||||
|
|
||||||
|
Fixed removing orientation in ImageOps.exif_transpose
|
||||||
|
=====================================================
|
||||||
|
|
||||||
|
In 8.3.0, :py:meth:`~PIL.ImageOps.exif_transpose` was changed to ensure that the
|
||||||
|
original image EXIF data was not modified, and the orientation was only removed from
|
||||||
|
the modified copy.
|
||||||
|
|
||||||
|
However, for certain images the orientation was already missing from the modified
|
||||||
|
image, leading to a KeyError.
|
||||||
|
|
||||||
|
This error has been resolved, and the copying of metadata to the modified image
|
||||||
|
improved.
|
41
docs/releasenotes/8.3.2.rst
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
8.3.2
|
||||||
|
-----
|
||||||
|
|
||||||
|
Security
|
||||||
|
========
|
||||||
|
|
||||||
|
* :cve:`CVE-2021-23437`: Avoid a potential ReDoS (regular expression denial of service)
|
||||||
|
in :py:class:`~PIL.ImageColor`'s :py:meth:`~PIL.ImageColor.getrgb` by raising
|
||||||
|
:py:exc:`ValueError` if the color specifier is too long. Present since Pillow 5.2.0.
|
||||||
|
|
||||||
|
* Fix 6-byte out-of-bounds (OOB) read. The previous bounds check in ``FliDecode.c``
|
||||||
|
incorrectly calculated the required read buffer size when copying a chunk, potentially
|
||||||
|
reading six extra bytes off the end of the allocated buffer from the heap. Present
|
||||||
|
since Pillow 7.1.0. This bug was found by Google's `OSS-Fuzz`_ `CIFuzz`_ runs.
|
||||||
|
|
||||||
|
Other Changes
|
||||||
|
=============
|
||||||
|
|
||||||
|
Python 3.10 wheels
|
||||||
|
^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
Pillow now includes binary wheels for Python 3.10.
|
||||||
|
|
||||||
|
The Python 3.10 release candidate was released on 2021-08-03 with the final release due
|
||||||
|
2021-10-04 (:pep:`619`). The CPython core team strongly encourages maintainers of
|
||||||
|
third-party Python projects to prepare for 3.10 compatibility. And as there are `no ABI
|
||||||
|
changes`_ planned we are releasing wheels to help others prepare for 3.10, and ensure
|
||||||
|
Pillow can be used immediately on release day of 3.10.0 final.
|
||||||
|
|
||||||
|
Fixed regressions
|
||||||
|
^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
* Ensure TIFF ``RowsPerStrip`` is multiple of 8 for JPEG compression (:pr:`5588`).
|
||||||
|
|
||||||
|
* Updates for :py:class:`~PIL.ImagePalette` channel order (:pr:`5599`).
|
||||||
|
|
||||||
|
* Hide FriBiDi shim symbols to avoid conflict with real FriBiDi library (:pr:`5651`).
|
||||||
|
|
||||||
|
.. _OSS-Fuzz: https://github.com/google/oss-fuzz
|
||||||
|
.. _CIFuzz: https://google.github.io/oss-fuzz/getting-started/continuous-integration/
|
||||||
|
.. _no ABI changes: https://www.python.org/downloads/release/python-3100rc1/
|
61
docs/releasenotes/8.4.0.rst
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
8.4.0
|
||||||
|
-----
|
||||||
|
|
||||||
|
API Changes
|
||||||
|
===========
|
||||||
|
|
||||||
|
Deprecations
|
||||||
|
^^^^^^^^^^^^
|
||||||
|
|
||||||
|
ImagePalette size parameter
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
The ``size`` parameter will be removed in Pillow 10.0.0 (2023-01-02).
|
||||||
|
|
||||||
|
Before Pillow 8.3.0, ``ImagePalette`` required palette data of particular lengths by
|
||||||
|
default, and the size parameter could be used to override that. Pillow 8.3.0 removed
|
||||||
|
the default required length, also removing the need for the size parameter.
|
||||||
|
|
||||||
|
API Additions
|
||||||
|
=============
|
||||||
|
|
||||||
|
Added "transparency" argument for loading EPS images
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
This new argument switches the Ghostscript device from "ppmraw" to "pngalpha",
|
||||||
|
generating an RGBA image with a transparent background instead of an RGB image with a
|
||||||
|
white background.
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
with Image.open("sample.eps") as im:
|
||||||
|
im.load(transparency=True)
|
||||||
|
|
||||||
|
Added WalImageFile class
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
:py:func:`PIL.WalImageFile.open()` previously returned a generic
|
||||||
|
:py:class:`PIL.Image.Image` instance. It now returns a dedicated
|
||||||
|
:py:class:`PIL.WalImageFile.WalImageFile` class.
|
||||||
|
|
||||||
|
Security
|
||||||
|
========
|
||||||
|
|
||||||
|
TODO
|
||||||
|
^^^^
|
||||||
|
|
||||||
|
TODO
|
||||||
|
|
||||||
|
Other Changes
|
||||||
|
=============
|
||||||
|
|
||||||
|
Speed improvement when rotating square images
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
Starting with Pillow 3.3.0, the speed of rotating images by 90 or 270 degrees was
|
||||||
|
improved by quickly returning :py:meth:`~PIL.Image.Image.transpose` instead, if the
|
||||||
|
rotate operation allowed for expansion and did not specify a center or post-rotate
|
||||||
|
translation.
|
||||||
|
|
||||||
|
Since the ``expand`` flag makes no difference for square images though, Pillow now
|
||||||
|
uses this faster method for square images without the ``expand`` flag as well.
|
|
@ -14,6 +14,9 @@ expected to be backported to earlier versions.
|
||||||
.. toctree::
|
.. toctree::
|
||||||
:maxdepth: 2
|
:maxdepth: 2
|
||||||
|
|
||||||
|
8.4.0
|
||||||
|
8.3.2
|
||||||
|
8.3.1
|
||||||
8.3.0
|
8.3.0
|
||||||
8.2.0
|
8.2.0
|
||||||
8.1.2
|
8.1.2
|
||||||
|
|
2
setup.py
|
@ -533,6 +533,8 @@ class pil_build_ext(build_ext):
|
||||||
_add_directory(include_dirs, "/usr/X11/include")
|
_add_directory(include_dirs, "/usr/X11/include")
|
||||||
|
|
||||||
# SDK install path
|
# SDK install path
|
||||||
|
sdk_path = "/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk"
|
||||||
|
if not os.path.exists(sdk_path):
|
||||||
try:
|
try:
|
||||||
sdk_path = (
|
sdk_path = (
|
||||||
subprocess.check_output(["xcrun", "--show-sdk-path"])
|
subprocess.check_output(["xcrun", "--show-sdk-path"])
|
||||||
|
|
|
@ -58,7 +58,7 @@ def _dib_accept(prefix):
|
||||||
# Image plugin for the Windows BMP format.
|
# Image plugin for the Windows BMP format.
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
class BmpImageFile(ImageFile.ImageFile):
|
class BmpImageFile(ImageFile.ImageFile):
|
||||||
""" Image plugin for the Windows Bitmap format (BMP) """
|
"""Image plugin for the Windows Bitmap format (BMP)"""
|
||||||
|
|
||||||
# ------------------------------------------------------------- Description
|
# ------------------------------------------------------------- Description
|
||||||
format_description = "Windows Bitmap"
|
format_description = "Windows Bitmap"
|
||||||
|
@ -70,7 +70,7 @@ class BmpImageFile(ImageFile.ImageFile):
|
||||||
vars()[k] = v
|
vars()[k] = v
|
||||||
|
|
||||||
def _bitmap(self, header=0, offset=0):
|
def _bitmap(self, header=0, offset=0):
|
||||||
""" Read relevant info about the BMP """
|
"""Read relevant info about the BMP"""
|
||||||
read, seek = self.fp.read, self.fp.seek
|
read, seek = self.fp.read, self.fp.seek
|
||||||
if header:
|
if header:
|
||||||
seek(header)
|
seek(header)
|
||||||
|
@ -257,7 +257,7 @@ class BmpImageFile(ImageFile.ImageFile):
|
||||||
]
|
]
|
||||||
|
|
||||||
def _open(self):
|
def _open(self):
|
||||||
""" Open file, check magic number and read header """
|
"""Open file, check magic number and read header"""
|
||||||
# read 14 bytes: magic number, filesize, reserved, header final offset
|
# read 14 bytes: magic number, filesize, reserved, header final offset
|
||||||
head_data = self.fp.read(14)
|
head_data = self.fp.read(14)
|
||||||
# choke if the file does not have the required magic bytes
|
# choke if the file does not have the required magic bytes
|
||||||
|
|
|
@ -61,7 +61,7 @@ def has_ghostscript():
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def Ghostscript(tile, size, fp, scale=1):
|
def Ghostscript(tile, size, fp, scale=1, transparency=False):
|
||||||
"""Render an image using Ghostscript"""
|
"""Render an image using Ghostscript"""
|
||||||
|
|
||||||
# Unpack decoder tile
|
# Unpack decoder tile
|
||||||
|
@ -108,6 +108,8 @@ def Ghostscript(tile, size, fp, scale=1):
|
||||||
lengthfile -= len(s)
|
lengthfile -= len(s)
|
||||||
f.write(s)
|
f.write(s)
|
||||||
|
|
||||||
|
device = "pngalpha" if transparency else "ppmraw"
|
||||||
|
|
||||||
# Build Ghostscript command
|
# Build Ghostscript command
|
||||||
command = [
|
command = [
|
||||||
"gs",
|
"gs",
|
||||||
|
@ -117,7 +119,7 @@ def Ghostscript(tile, size, fp, scale=1):
|
||||||
"-dBATCH", # exit after processing
|
"-dBATCH", # exit after processing
|
||||||
"-dNOPAUSE", # don't pause between pages
|
"-dNOPAUSE", # don't pause between pages
|
||||||
"-dSAFER", # safe mode
|
"-dSAFER", # safe mode
|
||||||
"-sDEVICE=ppmraw", # ppm driver
|
f"-sDEVICE={device}",
|
||||||
f"-sOutputFile={outfile}", # output file
|
f"-sOutputFile={outfile}", # output file
|
||||||
# adjust for image origin
|
# adjust for image origin
|
||||||
"-c",
|
"-c",
|
||||||
|
@ -325,11 +327,11 @@ class EpsImageFile(ImageFile.ImageFile):
|
||||||
|
|
||||||
return (length, offset)
|
return (length, offset)
|
||||||
|
|
||||||
def load(self, scale=1):
|
def load(self, scale=1, transparency=False):
|
||||||
# Load EPS via Ghostscript
|
# Load EPS via Ghostscript
|
||||||
if not self.tile:
|
if not self.tile:
|
||||||
return
|
return
|
||||||
self.im = Ghostscript(self.tile, self.size, self.fp, scale)
|
self.im = Ghostscript(self.tile, self.size, self.fp, scale, transparency)
|
||||||
self.mode = self.im.mode
|
self.mode = self.im.mode
|
||||||
self._size = self.im.size
|
self._size = self.im.size
|
||||||
self.tile = []
|
self.tile = []
|
||||||
|
|
|
@ -396,15 +396,7 @@ def _normalize_palette(im, palette, info):
|
||||||
if isinstance(palette, (bytes, bytearray, list)):
|
if isinstance(palette, (bytes, bytearray, list)):
|
||||||
source_palette = bytearray(palette[:768])
|
source_palette = bytearray(palette[:768])
|
||||||
if isinstance(palette, ImagePalette.ImagePalette):
|
if isinstance(palette, ImagePalette.ImagePalette):
|
||||||
source_palette = bytearray(
|
source_palette = bytearray(palette.palette)
|
||||||
itertools.chain.from_iterable(
|
|
||||||
zip(
|
|
||||||
palette.palette[:256],
|
|
||||||
palette.palette[256:512],
|
|
||||||
palette.palette[512:768],
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
if im.mode == "P":
|
if im.mode == "P":
|
||||||
if not source_palette:
|
if not source_palette:
|
||||||
|
@ -414,6 +406,23 @@ def _normalize_palette(im, palette, info):
|
||||||
source_palette = bytearray(i // 3 for i in range(768))
|
source_palette = bytearray(i // 3 for i in range(768))
|
||||||
im.palette = ImagePalette.ImagePalette("RGB", palette=source_palette)
|
im.palette = ImagePalette.ImagePalette("RGB", palette=source_palette)
|
||||||
|
|
||||||
|
if palette:
|
||||||
|
used_palette_colors = []
|
||||||
|
for i in range(0, len(source_palette), 3):
|
||||||
|
source_color = tuple(source_palette[i : i + 3])
|
||||||
|
try:
|
||||||
|
index = im.palette.colors[source_color]
|
||||||
|
except KeyError:
|
||||||
|
index = None
|
||||||
|
used_palette_colors.append(index)
|
||||||
|
for i, index in enumerate(used_palette_colors):
|
||||||
|
if index is None:
|
||||||
|
for j in range(len(used_palette_colors)):
|
||||||
|
if j not in used_palette_colors:
|
||||||
|
used_palette_colors[i] = j
|
||||||
|
break
|
||||||
|
im = im.remap_palette(used_palette_colors)
|
||||||
|
else:
|
||||||
used_palette_colors = _get_optimize(im, info)
|
used_palette_colors = _get_optimize(im, info)
|
||||||
if used_palette_colors is not None:
|
if used_palette_colors is not None:
|
||||||
return im.remap_palette(used_palette_colors, source_palette)
|
return im.remap_palette(used_palette_colors, source_palette)
|
||||||
|
@ -507,6 +516,7 @@ def _write_multiple_frames(im, fp, palette):
|
||||||
offset = (0, 0)
|
offset = (0, 0)
|
||||||
else:
|
else:
|
||||||
# compress difference
|
# compress difference
|
||||||
|
if not palette:
|
||||||
frame_data["encoderinfo"]["include_color_table"] = True
|
frame_data["encoderinfo"]["include_color_table"] = True
|
||||||
|
|
||||||
im_frame = im_frame.crop(frame_data["bbox"])
|
im_frame = im_frame.crop(frame_data["bbox"])
|
||||||
|
@ -787,7 +797,7 @@ def _get_global_header(im, info):
|
||||||
"""Return a list of strings representing a GIF header"""
|
"""Return a list of strings representing a GIF header"""
|
||||||
|
|
||||||
# Header Block
|
# Header Block
|
||||||
# http://www.matthewflickinger.com/lab/whatsinagif/bits_and_bytes.asp
|
# https://www.matthewflickinger.com/lab/whatsinagif/bits_and_bytes.asp
|
||||||
|
|
||||||
version = b"87a"
|
version = b"87a"
|
||||||
for extensionKey in ["transparency", "duration", "loop", "comment"]:
|
for extensionKey in ["transparency", "duration", "loop", "comment"]:
|
||||||
|
|
|
@ -235,8 +235,8 @@ class IcoFile:
|
||||||
# the total mask data is
|
# the total mask data is
|
||||||
# padded row size * height / bits per char
|
# padded row size * height / bits per char
|
||||||
|
|
||||||
and_mask_offset = o + int(im.size[0] * im.size[1] * (bpp / 8.0))
|
|
||||||
total_bytes = int((w * im.size[1]) / 8)
|
total_bytes = int((w * im.size[1]) / 8)
|
||||||
|
and_mask_offset = header["offset"] + header["size"] - total_bytes
|
||||||
|
|
||||||
self.buf.seek(and_mask_offset)
|
self.buf.seek(and_mask_offset)
|
||||||
mask_data = self.buf.read(total_bytes)
|
mask_data = self.buf.read(total_bytes)
|
||||||
|
|
|
@ -681,7 +681,7 @@ class Image:
|
||||||
raise ValueError("Could not save to PNG for display") from e
|
raise ValueError("Could not save to PNG for display") from e
|
||||||
return b.getvalue()
|
return b.getvalue()
|
||||||
|
|
||||||
def __array__(self):
|
def __array__(self, dtype=None):
|
||||||
# numpy array interface support
|
# numpy array interface support
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
|
@ -700,7 +700,7 @@ class Image:
|
||||||
class ArrayData:
|
class ArrayData:
|
||||||
__array_interface__ = new
|
__array_interface__ = new
|
||||||
|
|
||||||
return np.array(ArrayData())
|
return np.array(ArrayData(), dtype)
|
||||||
|
|
||||||
def __getstate__(self):
|
def __getstate__(self):
|
||||||
return [self.info, self.mode, self.size, self.getpalette(), self.tobytes()]
|
return [self.info, self.mode, self.size, self.getpalette(), self.tobytes()]
|
||||||
|
@ -914,16 +914,18 @@ class Image:
|
||||||
|
|
||||||
self.load()
|
self.load()
|
||||||
|
|
||||||
|
has_transparency = self.info.get("transparency") is not None
|
||||||
if not mode and self.mode == "P":
|
if not mode and self.mode == "P":
|
||||||
# determine default mode
|
# determine default mode
|
||||||
if self.palette:
|
if self.palette:
|
||||||
mode = self.palette.mode
|
mode = self.palette.mode
|
||||||
else:
|
else:
|
||||||
mode = "RGB"
|
mode = "RGB"
|
||||||
|
if mode == "RGB" and has_transparency:
|
||||||
|
mode = "RGBA"
|
||||||
if not mode or (mode == self.mode and not matrix):
|
if not mode or (mode == self.mode and not matrix):
|
||||||
return self.copy()
|
return self.copy()
|
||||||
|
|
||||||
has_transparency = self.info.get("transparency") is not None
|
|
||||||
if matrix:
|
if matrix:
|
||||||
# matrix conversion
|
# matrix conversion
|
||||||
if mode not in ("L", "RGB"):
|
if mode not in ("L", "RGB"):
|
||||||
|
@ -1005,7 +1007,7 @@ class Image:
|
||||||
trns_im = trns_im.convert("RGB")
|
trns_im = trns_im.convert("RGB")
|
||||||
trns = trns_im.getpixel((0, 0))
|
trns = trns_im.getpixel((0, 0))
|
||||||
|
|
||||||
elif self.mode == "P" and mode == "RGBA":
|
elif self.mode == "P" and mode in ("LA", "PA", "RGBA"):
|
||||||
t = self.info["transparency"]
|
t = self.info["transparency"]
|
||||||
delete_trns = True
|
delete_trns = True
|
||||||
|
|
||||||
|
@ -1128,7 +1130,9 @@ class Image:
|
||||||
"only RGB or L mode images can be quantized to a palette"
|
"only RGB or L mode images can be quantized to a palette"
|
||||||
)
|
)
|
||||||
im = self.im.convert("P", dither, palette.im)
|
im = self.im.convert("P", dither, palette.im)
|
||||||
return self._new(im)
|
new_im = self._new(im)
|
||||||
|
new_im.palette = palette.palette.copy()
|
||||||
|
return new_im
|
||||||
|
|
||||||
im = self._new(self.im.quantize(colors, method, kmeans))
|
im = self._new(self.im.quantize(colors, method, kmeans))
|
||||||
|
|
||||||
|
@ -1751,14 +1755,19 @@ class Image:
|
||||||
Attaches a palette to this image. The image must be a "P", "PA", "L"
|
Attaches a palette to this image. The image must be a "P", "PA", "L"
|
||||||
or "LA" image.
|
or "LA" image.
|
||||||
|
|
||||||
The palette sequence must contain at most 768 integer values, or 1024
|
The palette sequence must contain at most 256 colors, made up of one
|
||||||
integer values if alpha is included. Each group of values represents
|
integer value for each channel in the raw mode.
|
||||||
the red, green, blue (and alpha if included) values for the
|
For example, if the raw mode is "RGB", then it can contain at most 768
|
||||||
corresponding pixel index. Instead of an integer sequence, you can use
|
values, made up of red, green and blue values for the corresponding pixel
|
||||||
an 8-bit string.
|
index in the 256 colors.
|
||||||
|
If the raw mode is "RGBA", then it can contain at most 1024 values,
|
||||||
|
containing red, green, blue and alpha values.
|
||||||
|
|
||||||
|
Alternatively, an 8-bit string may be used instead of an integer sequence.
|
||||||
|
|
||||||
:param data: A palette sequence (either a list or a string).
|
:param data: A palette sequence (either a list or a string).
|
||||||
:param rawmode: The raw mode of the palette.
|
:param rawmode: The raw mode of the palette. Either "RGB", "RGBA", or a
|
||||||
|
mode that can be transformed to "RGB" (e.g. "R", "BGR;15", "RGBA;L").
|
||||||
"""
|
"""
|
||||||
from . import ImagePalette
|
from . import ImagePalette
|
||||||
|
|
||||||
|
@ -1832,18 +1841,16 @@ class Image:
|
||||||
if source_palette is None:
|
if source_palette is None:
|
||||||
if self.mode == "P":
|
if self.mode == "P":
|
||||||
self.load()
|
self.load()
|
||||||
real_source_palette = self.im.getpalette("RGB")[:768]
|
source_palette = self.im.getpalette("RGB")[:768]
|
||||||
else: # L-mode
|
else: # L-mode
|
||||||
real_source_palette = bytearray(i // 3 for i in range(768))
|
source_palette = bytearray(i // 3 for i in range(768))
|
||||||
else:
|
|
||||||
real_source_palette = source_palette
|
|
||||||
|
|
||||||
palette_bytes = b""
|
palette_bytes = b""
|
||||||
new_positions = [0] * 256
|
new_positions = [0] * 256
|
||||||
|
|
||||||
# pick only the used colors from the palette
|
# pick only the used colors from the palette
|
||||||
for i, oldPosition in enumerate(dest_map):
|
for i, oldPosition in enumerate(dest_map):
|
||||||
palette_bytes += real_source_palette[oldPosition * 3 : oldPosition * 3 + 3]
|
palette_bytes += source_palette[oldPosition * 3 : oldPosition * 3 + 3]
|
||||||
new_positions[oldPosition] = i
|
new_positions[oldPosition] = i
|
||||||
|
|
||||||
# replace the palette color id of all pixel with the new id
|
# replace the palette color id of all pixel with the new id
|
||||||
|
@ -2076,10 +2083,8 @@ class Image:
|
||||||
return self.copy()
|
return self.copy()
|
||||||
if angle == 180:
|
if angle == 180:
|
||||||
return self.transpose(ROTATE_180)
|
return self.transpose(ROTATE_180)
|
||||||
if angle == 90 and expand:
|
if angle in (90, 270) and (expand or self.width == self.height):
|
||||||
return self.transpose(ROTATE_90)
|
return self.transpose(ROTATE_90 if angle == 90 else ROTATE_270)
|
||||||
if angle == 270 and expand:
|
|
||||||
return self.transpose(ROTATE_270)
|
|
||||||
|
|
||||||
# Calculate the affine matrix. Note that this is the reverse
|
# Calculate the affine matrix. Note that this is the reverse
|
||||||
# transformation (from destination image to source) because we
|
# transformation (from destination image to source) because we
|
||||||
|
@ -2182,12 +2187,12 @@ class Image:
|
||||||
|
|
||||||
filename = ""
|
filename = ""
|
||||||
open_fp = False
|
open_fp = False
|
||||||
if isPath(fp):
|
if isinstance(fp, Path):
|
||||||
filename = fp
|
|
||||||
open_fp = True
|
|
||||||
elif isinstance(fp, Path):
|
|
||||||
filename = str(fp)
|
filename = str(fp)
|
||||||
open_fp = True
|
open_fp = True
|
||||||
|
elif isPath(fp):
|
||||||
|
filename = fp
|
||||||
|
open_fp = True
|
||||||
elif fp == sys.stdout:
|
elif fp == sys.stdout:
|
||||||
try:
|
try:
|
||||||
fp = sys.stdout.buffer
|
fp = sys.stdout.buffer
|
||||||
|
@ -2481,6 +2486,8 @@ class Image:
|
||||||
raise ValueError("missing method data")
|
raise ValueError("missing method data")
|
||||||
|
|
||||||
im = new(self.mode, size, fillcolor)
|
im = new(self.mode, size, fillcolor)
|
||||||
|
if self.mode == "P" and self.palette:
|
||||||
|
im.palette = self.palette.copy()
|
||||||
im.info = self.info.copy()
|
im.info = self.info.copy()
|
||||||
if method == MESH:
|
if method == MESH:
|
||||||
# list of quads
|
# list of quads
|
||||||
|
|
|
@ -37,7 +37,7 @@ pyCMS
|
||||||
http://www.cazabon.com
|
http://www.cazabon.com
|
||||||
|
|
||||||
pyCMS home page: http://www.cazabon.com/pyCMS
|
pyCMS home page: http://www.cazabon.com/pyCMS
|
||||||
littleCMS home page: http://www.littlecms.com
|
littleCMS home page: https://www.littlecms.com
|
||||||
(littleCMS is Copyright (C) 1998-2001 Marti Maria)
|
(littleCMS is Copyright (C) 1998-2001 Marti Maria)
|
||||||
|
|
||||||
Originally released under LGPL. Graciously donated to PIL in
|
Originally released under LGPL. Graciously donated to PIL in
|
||||||
|
|
|
@ -32,6 +32,8 @@ def getrgb(color):
|
||||||
:param color: A color string
|
:param color: A color string
|
||||||
:return: ``(red, green, blue[, alpha])``
|
:return: ``(red, green, blue[, alpha])``
|
||||||
"""
|
"""
|
||||||
|
if len(color) > 100:
|
||||||
|
raise ValueError("color specifier is too long")
|
||||||
color = color.lower()
|
color = color.lower()
|
||||||
|
|
||||||
rgb = colormap.get(color, None)
|
rgb = colormap.get(color, None)
|
||||||
|
|
|
@ -493,7 +493,11 @@ def _save(im, fp, tile, bufsize=0):
|
||||||
# But, it would need at least the image size in most cases. RawEncode is
|
# But, it would need at least the image size in most cases. RawEncode is
|
||||||
# a tricky case.
|
# a tricky case.
|
||||||
bufsize = max(MAXBLOCK, bufsize, im.size[0] * 4) # see RawEncode.c
|
bufsize = max(MAXBLOCK, bufsize, im.size[0] * 4) # see RawEncode.c
|
||||||
if fp == sys.stdout or (hasattr(sys.stdout, "buffer") and fp == sys.stdout.buffer):
|
try:
|
||||||
|
stdout = fp == sys.stdout or fp == sys.stdout.buffer
|
||||||
|
except (OSError, AttributeError):
|
||||||
|
stdout = False
|
||||||
|
if stdout:
|
||||||
fp.flush()
|
fp.flush()
|
||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
|
|
|
@ -19,8 +19,9 @@
|
||||||
|
|
||||||
import functools
|
import functools
|
||||||
import operator
|
import operator
|
||||||
|
import re
|
||||||
|
|
||||||
from . import Image, ImageDraw
|
from . import Image
|
||||||
|
|
||||||
#
|
#
|
||||||
# helpers
|
# helpers
|
||||||
|
@ -394,14 +395,15 @@ def expand(image, border=0, fill=0):
|
||||||
height = top + image.size[1] + bottom
|
height = top + image.size[1] + bottom
|
||||||
color = _color(fill, image.mode)
|
color = _color(fill, image.mode)
|
||||||
if image.mode == "P" and image.palette:
|
if image.mode == "P" and image.palette:
|
||||||
out = Image.new(image.mode, (width, height))
|
image.load()
|
||||||
out.putpalette(image.palette)
|
palette = image.palette.copy()
|
||||||
out.paste(image, (left, top))
|
if isinstance(color, tuple):
|
||||||
|
color = palette.getcolor(color)
|
||||||
draw = ImageDraw.Draw(out)
|
|
||||||
draw.rectangle((0, 0, width - 1, height - 1), outline=color, width=border)
|
|
||||||
else:
|
else:
|
||||||
|
palette = None
|
||||||
out = Image.new(image.mode, (width, height), color)
|
out = Image.new(image.mode, (width, height), color)
|
||||||
|
if palette:
|
||||||
|
out.putpalette(palette.palette)
|
||||||
out.paste(image, (left, top))
|
out.paste(image, (left, top))
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
@ -588,7 +590,19 @@ def exif_transpose(image):
|
||||||
if method is not None:
|
if method is not None:
|
||||||
transposed_image = image.transpose(method)
|
transposed_image = image.transpose(method)
|
||||||
transposed_exif = transposed_image.getexif()
|
transposed_exif = transposed_image.getexif()
|
||||||
|
if 0x0112 in transposed_exif:
|
||||||
del transposed_exif[0x0112]
|
del transposed_exif[0x0112]
|
||||||
|
if "exif" in transposed_image.info:
|
||||||
transposed_image.info["exif"] = transposed_exif.tobytes()
|
transposed_image.info["exif"] = transposed_exif.tobytes()
|
||||||
|
elif "Raw profile type exif" in transposed_image.info:
|
||||||
|
transposed_image.info[
|
||||||
|
"Raw profile type exif"
|
||||||
|
] = transposed_exif.tobytes().hex()
|
||||||
|
elif "XML:com.adobe.xmp" in transposed_image.info:
|
||||||
|
transposed_image.info["XML:com.adobe.xmp"] = re.sub(
|
||||||
|
r'tiff:Orientation="([0-9])"',
|
||||||
|
"",
|
||||||
|
transposed_image.info["XML:com.adobe.xmp"],
|
||||||
|
)
|
||||||
return transposed_image
|
return transposed_image
|
||||||
return image.copy()
|
return image.copy()
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
#
|
#
|
||||||
|
|
||||||
import array
|
import array
|
||||||
|
import warnings
|
||||||
|
|
||||||
from . import GimpGradientFile, GimpPaletteFile, ImageColor, PaletteFile
|
from . import GimpGradientFile, GimpPaletteFile, ImageColor, PaletteFile
|
||||||
|
|
||||||
|
@ -25,15 +26,14 @@ class ImagePalette:
|
||||||
"""
|
"""
|
||||||
Color palette for palette mapped images
|
Color palette for palette mapped images
|
||||||
|
|
||||||
:param mode: The mode to use for the Palette. See:
|
:param mode: The mode to use for the palette. See:
|
||||||
:ref:`concept-modes`. Defaults to "RGB"
|
:ref:`concept-modes`. Defaults to "RGB"
|
||||||
:param palette: An optional palette. If given, it must be a bytearray,
|
:param palette: An optional palette. If given, it must be a bytearray,
|
||||||
an array or a list of ints between 0-255 and of length ``size``
|
an array or a list of ints between 0-255. The list must consist of
|
||||||
times the number of colors in ``mode``. The list must be aligned
|
all channels for one color followed by the next color (e.g. RGBRGBRGB).
|
||||||
by channel (All R values must be contiguous in the list before G
|
Defaults to an empty palette.
|
||||||
and B values.) Defaults to 0 through 255 per channel.
|
:param size: An optional palette size. If given, an error is raised
|
||||||
:param size: An optional palette size. If given, it cannot be equal to
|
if ``palette`` is not of equal length.
|
||||||
or greater than 256. Defaults to 0.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, mode="RGB", palette=None, size=0):
|
def __init__(self, mode="RGB", palette=None, size=0):
|
||||||
|
@ -41,7 +41,13 @@ class ImagePalette:
|
||||||
self.rawmode = None # if set, palette contains raw data
|
self.rawmode = None # if set, palette contains raw data
|
||||||
self.palette = palette or bytearray()
|
self.palette = palette or bytearray()
|
||||||
self.dirty = None
|
self.dirty = None
|
||||||
if size != 0 and size != len(self.palette):
|
if size != 0:
|
||||||
|
warnings.warn(
|
||||||
|
"The size parameter is deprecated and will be removed in Pillow 10 "
|
||||||
|
"(2023-01-02).",
|
||||||
|
DeprecationWarning,
|
||||||
|
)
|
||||||
|
if size != len(self.palette):
|
||||||
raise ValueError("wrong palette size")
|
raise ValueError("wrong palette size")
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -205,9 +211,9 @@ def make_gamma_lut(exp):
|
||||||
|
|
||||||
|
|
||||||
def negative(mode="RGB"):
|
def negative(mode="RGB"):
|
||||||
palette = list(range(256))
|
palette = list(range(256 * len(mode)))
|
||||||
palette.reverse()
|
palette.reverse()
|
||||||
return ImagePalette(mode, palette * len(mode))
|
return ImagePalette(mode, [i // len(mode) for i in palette])
|
||||||
|
|
||||||
|
|
||||||
def random(mode="RGB"):
|
def random(mode="RGB"):
|
||||||
|
@ -220,15 +226,13 @@ def random(mode="RGB"):
|
||||||
|
|
||||||
|
|
||||||
def sepia(white="#fff0c0"):
|
def sepia(white="#fff0c0"):
|
||||||
r, g, b = ImageColor.getrgb(white)
|
bands = [make_linear_lut(0, band) for band in ImageColor.getrgb(white)]
|
||||||
r = make_linear_lut(0, r)
|
return ImagePalette("RGB", [bands[i % 3][i // 3] for i in range(256 * 3)])
|
||||||
g = make_linear_lut(0, g)
|
|
||||||
b = make_linear_lut(0, b)
|
|
||||||
return ImagePalette("RGB", r + g + b)
|
|
||||||
|
|
||||||
|
|
||||||
def wedge(mode="RGB"):
|
def wedge(mode="RGB"):
|
||||||
return ImagePalette(mode, list(range(256)) * len(mode))
|
palette = list(range(256 * len(mode)))
|
||||||
|
return ImagePalette(mode, [i // len(mode) for i in palette])
|
||||||
|
|
||||||
|
|
||||||
def load(filename):
|
def load(filename):
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
#
|
#
|
||||||
# History:
|
# History:
|
||||||
# 2014-03-12 ajh Created
|
# 2014-03-12 ajh Created
|
||||||
|
# 2021-06-30 rogermb Extract dpi information from the 'resc' header box
|
||||||
#
|
#
|
||||||
# Copyright (c) 2014 Coriolis Systems Limited
|
# Copyright (c) 2014 Coriolis Systems Limited
|
||||||
# Copyright (c) 2014 Alastair Houghton
|
# Copyright (c) 2014 Alastair Houghton
|
||||||
|
@ -19,6 +20,79 @@ import struct
|
||||||
from . import Image, ImageFile
|
from . import Image, ImageFile
|
||||||
|
|
||||||
|
|
||||||
|
class BoxReader:
|
||||||
|
"""
|
||||||
|
A small helper class to read fields stored in JPEG2000 header boxes
|
||||||
|
and to easily step into and read sub-boxes.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, fp, length=-1):
|
||||||
|
self.fp = fp
|
||||||
|
self.has_length = length >= 0
|
||||||
|
self.length = length
|
||||||
|
self.remaining_in_box = -1
|
||||||
|
|
||||||
|
def _can_read(self, num_bytes):
|
||||||
|
if self.has_length and self.fp.tell() + num_bytes > self.length:
|
||||||
|
# Outside box: ensure we don't read past the known file length
|
||||||
|
return False
|
||||||
|
if self.remaining_in_box >= 0:
|
||||||
|
# Inside box contents: ensure read does not go past box boundaries
|
||||||
|
return num_bytes <= self.remaining_in_box
|
||||||
|
else:
|
||||||
|
return True # No length known, just read
|
||||||
|
|
||||||
|
def _read_bytes(self, num_bytes):
|
||||||
|
if not self._can_read(num_bytes):
|
||||||
|
raise SyntaxError("Not enough data in header")
|
||||||
|
|
||||||
|
data = self.fp.read(num_bytes)
|
||||||
|
if len(data) < num_bytes:
|
||||||
|
raise OSError(
|
||||||
|
f"Expected to read {num_bytes} bytes but only got {len(data)}."
|
||||||
|
)
|
||||||
|
|
||||||
|
if self.remaining_in_box > 0:
|
||||||
|
self.remaining_in_box -= num_bytes
|
||||||
|
return data
|
||||||
|
|
||||||
|
def read_fields(self, field_format):
|
||||||
|
size = struct.calcsize(field_format)
|
||||||
|
data = self._read_bytes(size)
|
||||||
|
return struct.unpack(field_format, data)
|
||||||
|
|
||||||
|
def read_boxes(self):
|
||||||
|
size = self.remaining_in_box
|
||||||
|
data = self._read_bytes(size)
|
||||||
|
return BoxReader(io.BytesIO(data), size)
|
||||||
|
|
||||||
|
def has_next_box(self):
|
||||||
|
if self.has_length:
|
||||||
|
return self.fp.tell() + self.remaining_in_box < self.length
|
||||||
|
else:
|
||||||
|
return True
|
||||||
|
|
||||||
|
def next_box_type(self):
|
||||||
|
# Skip the rest of the box if it has not been read
|
||||||
|
if self.remaining_in_box > 0:
|
||||||
|
self.fp.seek(self.remaining_in_box, os.SEEK_CUR)
|
||||||
|
self.remaining_in_box = -1
|
||||||
|
|
||||||
|
# Read the length and type of the next box
|
||||||
|
lbox, tbox = self.read_fields(">I4s")
|
||||||
|
if lbox == 1:
|
||||||
|
lbox = self.read_fields(">Q")[0]
|
||||||
|
hlen = 16
|
||||||
|
else:
|
||||||
|
hlen = 8
|
||||||
|
|
||||||
|
if lbox < hlen or not self._can_read(lbox - hlen):
|
||||||
|
raise SyntaxError("Invalid header length")
|
||||||
|
|
||||||
|
self.remaining_in_box = lbox - hlen
|
||||||
|
return tbox
|
||||||
|
|
||||||
|
|
||||||
def _parse_codestream(fp):
|
def _parse_codestream(fp):
|
||||||
"""Parse the JPEG 2000 codestream to extract the size and component
|
"""Parse the JPEG 2000 codestream to extract the size and component
|
||||||
count from the SIZ marker segment, returning a PIL (size, mode) tuple."""
|
count from the SIZ marker segment, returning a PIL (size, mode) tuple."""
|
||||||
|
@ -53,57 +127,45 @@ def _parse_codestream(fp):
|
||||||
return (size, mode)
|
return (size, mode)
|
||||||
|
|
||||||
|
|
||||||
|
def _res_to_dpi(num, denom, exp):
|
||||||
|
"""Convert JPEG2000's (numerator, denominator, exponent-base-10) resolution,
|
||||||
|
calculated as (num / denom) * 10^exp and stored in dots per meter,
|
||||||
|
to floating-point dots per inch."""
|
||||||
|
if denom != 0:
|
||||||
|
return (254 * num * (10 ** exp)) / (10000 * denom)
|
||||||
|
|
||||||
|
|
||||||
def _parse_jp2_header(fp):
|
def _parse_jp2_header(fp):
|
||||||
"""Parse the JP2 header box to extract size, component count and
|
"""Parse the JP2 header box to extract size, component count,
|
||||||
color space information, returning a (size, mode, mimetype) tuple."""
|
color space information, and optionally DPI information,
|
||||||
|
returning a (size, mode, mimetype, dpi) tuple."""
|
||||||
|
|
||||||
# Find the JP2 header box
|
# Find the JP2 header box
|
||||||
|
reader = BoxReader(fp)
|
||||||
header = None
|
header = None
|
||||||
mimetype = None
|
mimetype = None
|
||||||
while True:
|
while reader.has_next_box():
|
||||||
lbox, tbox = struct.unpack(">I4s", fp.read(8))
|
tbox = reader.next_box_type()
|
||||||
if lbox == 1:
|
|
||||||
lbox = struct.unpack(">Q", fp.read(8))[0]
|
|
||||||
hlen = 16
|
|
||||||
else:
|
|
||||||
hlen = 8
|
|
||||||
|
|
||||||
if lbox < hlen:
|
|
||||||
raise SyntaxError("Invalid JP2 header length")
|
|
||||||
|
|
||||||
if tbox == b"jp2h":
|
if tbox == b"jp2h":
|
||||||
header = fp.read(lbox - hlen)
|
header = reader.read_boxes()
|
||||||
break
|
break
|
||||||
elif tbox == b"ftyp":
|
elif tbox == b"ftyp":
|
||||||
if fp.read(4) == b"jpx ":
|
if reader.read_fields(">4s")[0] == b"jpx ":
|
||||||
mimetype = "image/jpx"
|
mimetype = "image/jpx"
|
||||||
fp.seek(lbox - hlen - 4, os.SEEK_CUR)
|
|
||||||
else:
|
|
||||||
fp.seek(lbox - hlen, os.SEEK_CUR)
|
|
||||||
|
|
||||||
if header is None:
|
|
||||||
raise SyntaxError("could not find JP2 header")
|
|
||||||
|
|
||||||
size = None
|
size = None
|
||||||
mode = None
|
mode = None
|
||||||
bpc = None
|
bpc = None
|
||||||
nc = None
|
nc = None
|
||||||
|
dpi = None # 2-tuple of DPI info, or None
|
||||||
|
|
||||||
hio = io.BytesIO(header)
|
while header.has_next_box():
|
||||||
while True:
|
tbox = header.next_box_type()
|
||||||
lbox, tbox = struct.unpack(">I4s", hio.read(8))
|
|
||||||
if lbox == 1:
|
|
||||||
lbox = struct.unpack(">Q", hio.read(8))[0]
|
|
||||||
hlen = 16
|
|
||||||
else:
|
|
||||||
hlen = 8
|
|
||||||
|
|
||||||
content = hio.read(lbox - hlen)
|
|
||||||
|
|
||||||
if tbox == b"ihdr":
|
if tbox == b"ihdr":
|
||||||
height, width, nc, bpc, c, unkc, ipr = struct.unpack(">IIHBBBB", content)
|
height, width, nc, bpc = header.read_fields(">IIHB")
|
||||||
size = (width, height)
|
size = (width, height)
|
||||||
if unkc:
|
|
||||||
if nc == 1 and (bpc & 0x7F) > 8:
|
if nc == 1 and (bpc & 0x7F) > 8:
|
||||||
mode = "I;16"
|
mode = "I;16"
|
||||||
elif nc == 1:
|
elif nc == 1:
|
||||||
|
@ -114,40 +176,22 @@ def _parse_jp2_header(fp):
|
||||||
mode = "RGB"
|
mode = "RGB"
|
||||||
elif nc == 4:
|
elif nc == 4:
|
||||||
mode = "RGBA"
|
mode = "RGBA"
|
||||||
break
|
elif tbox == b"res ":
|
||||||
elif tbox == b"colr":
|
res = header.read_boxes()
|
||||||
meth, prec, approx = struct.unpack_from(">BBB", content)
|
while res.has_next_box():
|
||||||
if meth == 1:
|
tres = res.next_box_type()
|
||||||
cs = struct.unpack_from(">I", content, 3)[0]
|
if tres == b"resc":
|
||||||
if cs == 16: # sRGB
|
vrcn, vrcd, hrcn, hrcd, vrce, hrce = res.read_fields(">HHHHBB")
|
||||||
if nc == 1 and (bpc & 0x7F) > 8:
|
hres = _res_to_dpi(hrcn, hrcd, hrce)
|
||||||
mode = "I;16"
|
vres = _res_to_dpi(vrcn, vrcd, vrce)
|
||||||
elif nc == 1:
|
if hres is not None and vres is not None:
|
||||||
mode = "L"
|
dpi = (hres, vres)
|
||||||
elif nc == 3:
|
|
||||||
mode = "RGB"
|
|
||||||
elif nc == 4:
|
|
||||||
mode = "RGBA"
|
|
||||||
break
|
|
||||||
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
|
|
||||||
if nc == 3:
|
|
||||||
mode = "RGB"
|
|
||||||
elif nc == 4:
|
|
||||||
mode = "RGBA"
|
|
||||||
break
|
break
|
||||||
|
|
||||||
if size is None or mode is None:
|
if size is None or mode is None:
|
||||||
raise SyntaxError("Malformed jp2 header")
|
raise SyntaxError("Malformed JP2 header")
|
||||||
|
|
||||||
return (size, mode, mimetype)
|
return (size, mode, mimetype, dpi)
|
||||||
|
|
||||||
|
|
||||||
##
|
##
|
||||||
|
@ -169,7 +213,9 @@ class Jpeg2KImageFile(ImageFile.ImageFile):
|
||||||
if sig == b"\x00\x00\x00\x0cjP \x0d\x0a\x87\x0a":
|
if sig == b"\x00\x00\x00\x0cjP \x0d\x0a\x87\x0a":
|
||||||
self.codec = "jp2"
|
self.codec = "jp2"
|
||||||
header = _parse_jp2_header(self.fp)
|
header = _parse_jp2_header(self.fp)
|
||||||
self._size, self.mode, self.custom_mimetype = header
|
self._size, self.mode, self.custom_mimetype, dpi = header
|
||||||
|
if dpi is not None:
|
||||||
|
self.info["dpi"] = dpi
|
||||||
else:
|
else:
|
||||||
raise SyntaxError("not a JPEG 2000 file")
|
raise SyntaxError("not a JPEG 2000 file")
|
||||||
|
|
||||||
|
|
|
@ -168,11 +168,11 @@ def APP(self, marker):
|
||||||
# 1 dpcm = 2.54 dpi
|
# 1 dpcm = 2.54 dpi
|
||||||
dpi *= 2.54
|
dpi *= 2.54
|
||||||
self.info["dpi"] = dpi, dpi
|
self.info["dpi"] = dpi, dpi
|
||||||
except (KeyError, SyntaxError, ValueError, ZeroDivisionError):
|
except (TypeError, KeyError, SyntaxError, ValueError, ZeroDivisionError):
|
||||||
# SyntaxError for invalid/unreadable EXIF
|
# SyntaxError for invalid/unreadable EXIF
|
||||||
# KeyError for dpi not included
|
# KeyError for dpi not included
|
||||||
# ZeroDivisionError for invalid dpi rational value
|
# ZeroDivisionError for invalid dpi rational value
|
||||||
# ValueError for dpi being an invalid float
|
# ValueError or TypeError for dpi being an invalid float
|
||||||
self.info["dpi"] = 72, 72
|
self.info["dpi"] = 72, 72
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -21,7 +21,7 @@
|
||||||
# Figure 205. Windows Paint Version 1: "DanM" Format
|
# Figure 205. Windows Paint Version 1: "DanM" Format
|
||||||
# Figure 206. Windows Paint Version 2: "LinS" Format. Used in Windows V2.03
|
# Figure 206. Windows Paint Version 2: "LinS" Format. Used in Windows V2.03
|
||||||
#
|
#
|
||||||
# See also: http://www.fileformat.info/format/mspaint/egff.htm
|
# See also: https://www.fileformat.info/format/mspaint/egff.htm
|
||||||
|
|
||||||
import io
|
import io
|
||||||
import struct
|
import struct
|
||||||
|
@ -73,7 +73,7 @@ class MspImageFile(ImageFile.ImageFile):
|
||||||
|
|
||||||
class MspDecoder(ImageFile.PyDecoder):
|
class MspDecoder(ImageFile.PyDecoder):
|
||||||
# The algo for the MSP decoder is from
|
# The algo for the MSP decoder is from
|
||||||
# http://www.fileformat.info/format/mspaint/egff.htm
|
# https://www.fileformat.info/format/mspaint/egff.htm
|
||||||
# cc-by-attribution -- That page references is taken from the
|
# cc-by-attribution -- That page references is taken from the
|
||||||
# Encyclopedia of Graphics File Formats and is licensed by
|
# Encyclopedia of Graphics File Formats and is licensed by
|
||||||
# O'Reilly under the Creative Common/Attribution license
|
# O'Reilly under the Creative Common/Attribution license
|
||||||
|
|
|
@ -1061,8 +1061,10 @@ def _write_multiple_frames(im, fp, chunk, rawmode):
|
||||||
default_image = im.encoderinfo.get("default_image", im.info.get("default_image"))
|
default_image = im.encoderinfo.get("default_image", im.info.get("default_image"))
|
||||||
duration = im.encoderinfo.get("duration", im.info.get("duration", 0))
|
duration = im.encoderinfo.get("duration", im.info.get("duration", 0))
|
||||||
loop = im.encoderinfo.get("loop", im.info.get("loop", 0))
|
loop = im.encoderinfo.get("loop", im.info.get("loop", 0))
|
||||||
disposal = im.encoderinfo.get("disposal", im.info.get("disposal"))
|
disposal = im.encoderinfo.get(
|
||||||
blend = im.encoderinfo.get("blend", im.info.get("blend"))
|
"disposal", im.info.get("disposal", APNG_DISPOSE_OP_NONE)
|
||||||
|
)
|
||||||
|
blend = im.encoderinfo.get("blend", im.info.get("blend", APNG_BLEND_OP_SOURCE))
|
||||||
|
|
||||||
if default_image:
|
if default_image:
|
||||||
chain = itertools.chain(im.encoderinfo.get("append_images", []))
|
chain = itertools.chain(im.encoderinfo.get("append_images", []))
|
||||||
|
@ -1117,12 +1119,8 @@ def _write_multiple_frames(im, fp, chunk, rawmode):
|
||||||
and prev_disposal == encoderinfo.get("disposal")
|
and prev_disposal == encoderinfo.get("disposal")
|
||||||
and prev_blend == encoderinfo.get("blend")
|
and prev_blend == encoderinfo.get("blend")
|
||||||
):
|
):
|
||||||
duration = encoderinfo.get("duration", 0)
|
if isinstance(duration, (list, tuple)):
|
||||||
if duration:
|
previous["encoderinfo"]["duration"] += encoderinfo["duration"]
|
||||||
if "duration" in previous["encoderinfo"]:
|
|
||||||
previous["encoderinfo"]["duration"] += duration
|
|
||||||
else:
|
|
||||||
previous["encoderinfo"]["duration"] = duration
|
|
||||||
continue
|
continue
|
||||||
else:
|
else:
|
||||||
bbox = None
|
bbox = None
|
||||||
|
@ -1149,9 +1147,10 @@ def _write_multiple_frames(im, fp, chunk, rawmode):
|
||||||
bbox = frame_data["bbox"]
|
bbox = frame_data["bbox"]
|
||||||
im_frame = im_frame.crop(bbox)
|
im_frame = im_frame.crop(bbox)
|
||||||
size = im_frame.size
|
size = im_frame.size
|
||||||
duration = int(round(frame_data["encoderinfo"].get("duration", 0)))
|
encoderinfo = frame_data["encoderinfo"]
|
||||||
disposal = frame_data["encoderinfo"].get("disposal", APNG_DISPOSE_OP_NONE)
|
frame_duration = int(round(encoderinfo.get("duration", duration)))
|
||||||
blend = frame_data["encoderinfo"].get("blend", APNG_BLEND_OP_SOURCE)
|
frame_disposal = encoderinfo.get("disposal", disposal)
|
||||||
|
frame_blend = encoderinfo.get("blend", blend)
|
||||||
# frame control
|
# frame control
|
||||||
chunk(
|
chunk(
|
||||||
fp,
|
fp,
|
||||||
|
@ -1161,10 +1160,10 @@ def _write_multiple_frames(im, fp, chunk, rawmode):
|
||||||
o32(size[1]), # height
|
o32(size[1]), # height
|
||||||
o32(bbox[0]), # x_offset
|
o32(bbox[0]), # x_offset
|
||||||
o32(bbox[1]), # y_offset
|
o32(bbox[1]), # y_offset
|
||||||
o16(duration), # delay_numerator
|
o16(frame_duration), # delay_numerator
|
||||||
o16(1000), # delay_denominator
|
o16(1000), # delay_denominator
|
||||||
o8(disposal), # dispose_op
|
o8(frame_disposal), # dispose_op
|
||||||
o8(blend), # blend_op
|
o8(frame_blend), # blend_op
|
||||||
)
|
)
|
||||||
seq_num += 1
|
seq_num += 1
|
||||||
# frame data
|
# frame data
|
||||||
|
|
|
@ -22,6 +22,7 @@ from . import Image, ImageFile, ImagePalette
|
||||||
from ._binary import i8
|
from ._binary import i8
|
||||||
from ._binary import i16be as i16
|
from ._binary import i16be as i16
|
||||||
from ._binary import i32be as i32
|
from ._binary import i32be as i32
|
||||||
|
from ._binary import si16be as si16
|
||||||
|
|
||||||
MODES = {
|
MODES = {
|
||||||
# (photoshop mode, bits) -> (pil mode, required channels)
|
# (photoshop mode, bits) -> (pil mode, required channels)
|
||||||
|
@ -179,7 +180,7 @@ def _layerinfo(fp, ct_bytes):
|
||||||
def read(size):
|
def read(size):
|
||||||
return ImageFile._safe_read(fp, size)
|
return ImageFile._safe_read(fp, size)
|
||||||
|
|
||||||
ct = i16(read(2))
|
ct = si16(read(2))
|
||||||
|
|
||||||
# sanity check
|
# sanity check
|
||||||
if ct_bytes < (abs(ct) * 20):
|
if ct_bytes < (abs(ct) * 20):
|
||||||
|
|
|
@ -128,7 +128,7 @@ class PyAccess:
|
||||||
|
|
||||||
|
|
||||||
class _PyAccess32_2(PyAccess):
|
class _PyAccess32_2(PyAccess):
|
||||||
""" PA, LA, stored in first and last bytes of a 32 bit word """
|
"""PA, LA, stored in first and last bytes of a 32 bit word"""
|
||||||
|
|
||||||
def _post_init(self, *args, **kwargs):
|
def _post_init(self, *args, **kwargs):
|
||||||
self.pixels = ffi.cast("struct Pixel_RGBA **", self.image32)
|
self.pixels = ffi.cast("struct Pixel_RGBA **", self.image32)
|
||||||
|
@ -145,7 +145,7 @@ class _PyAccess32_2(PyAccess):
|
||||||
|
|
||||||
|
|
||||||
class _PyAccess32_3(PyAccess):
|
class _PyAccess32_3(PyAccess):
|
||||||
""" RGB and friends, stored in the first three bytes of a 32 bit word """
|
"""RGB and friends, stored in the first three bytes of a 32 bit word"""
|
||||||
|
|
||||||
def _post_init(self, *args, **kwargs):
|
def _post_init(self, *args, **kwargs):
|
||||||
self.pixels = ffi.cast("struct Pixel_RGBA **", self.image32)
|
self.pixels = ffi.cast("struct Pixel_RGBA **", self.image32)
|
||||||
|
@ -164,7 +164,7 @@ class _PyAccess32_3(PyAccess):
|
||||||
|
|
||||||
|
|
||||||
class _PyAccess32_4(PyAccess):
|
class _PyAccess32_4(PyAccess):
|
||||||
""" RGBA etc, all 4 bytes of a 32 bit word """
|
"""RGBA etc, all 4 bytes of a 32 bit word"""
|
||||||
|
|
||||||
def _post_init(self, *args, **kwargs):
|
def _post_init(self, *args, **kwargs):
|
||||||
self.pixels = ffi.cast("struct Pixel_RGBA **", self.image32)
|
self.pixels = ffi.cast("struct Pixel_RGBA **", self.image32)
|
||||||
|
@ -183,7 +183,7 @@ class _PyAccess32_4(PyAccess):
|
||||||
|
|
||||||
|
|
||||||
class _PyAccess8(PyAccess):
|
class _PyAccess8(PyAccess):
|
||||||
""" 1, L, P, 8 bit images stored as uint8 """
|
"""1, L, P, 8 bit images stored as uint8"""
|
||||||
|
|
||||||
def _post_init(self, *args, **kwargs):
|
def _post_init(self, *args, **kwargs):
|
||||||
self.pixels = self.image8
|
self.pixels = self.image8
|
||||||
|
@ -201,7 +201,7 @@ class _PyAccess8(PyAccess):
|
||||||
|
|
||||||
|
|
||||||
class _PyAccessI16_N(PyAccess):
|
class _PyAccessI16_N(PyAccess):
|
||||||
""" I;16 access, native bitendian without conversion """
|
"""I;16 access, native bitendian without conversion"""
|
||||||
|
|
||||||
def _post_init(self, *args, **kwargs):
|
def _post_init(self, *args, **kwargs):
|
||||||
self.pixels = ffi.cast("unsigned short **", self.image)
|
self.pixels = ffi.cast("unsigned short **", self.image)
|
||||||
|
@ -219,7 +219,7 @@ class _PyAccessI16_N(PyAccess):
|
||||||
|
|
||||||
|
|
||||||
class _PyAccessI16_L(PyAccess):
|
class _PyAccessI16_L(PyAccess):
|
||||||
""" I;16L access, with conversion """
|
"""I;16L access, with conversion"""
|
||||||
|
|
||||||
def _post_init(self, *args, **kwargs):
|
def _post_init(self, *args, **kwargs):
|
||||||
self.pixels = ffi.cast("struct Pixel_I16 **", self.image)
|
self.pixels = ffi.cast("struct Pixel_I16 **", self.image)
|
||||||
|
@ -240,7 +240,7 @@ class _PyAccessI16_L(PyAccess):
|
||||||
|
|
||||||
|
|
||||||
class _PyAccessI16_B(PyAccess):
|
class _PyAccessI16_B(PyAccess):
|
||||||
""" I;16B access, with conversion """
|
"""I;16B access, with conversion"""
|
||||||
|
|
||||||
def _post_init(self, *args, **kwargs):
|
def _post_init(self, *args, **kwargs):
|
||||||
self.pixels = ffi.cast("struct Pixel_I16 **", self.image)
|
self.pixels = ffi.cast("struct Pixel_I16 **", self.image)
|
||||||
|
@ -261,7 +261,7 @@ class _PyAccessI16_B(PyAccess):
|
||||||
|
|
||||||
|
|
||||||
class _PyAccessI32_N(PyAccess):
|
class _PyAccessI32_N(PyAccess):
|
||||||
""" Signed Int32 access, native endian """
|
"""Signed Int32 access, native endian"""
|
||||||
|
|
||||||
def _post_init(self, *args, **kwargs):
|
def _post_init(self, *args, **kwargs):
|
||||||
self.pixels = self.image32
|
self.pixels = self.image32
|
||||||
|
@ -274,7 +274,7 @@ class _PyAccessI32_N(PyAccess):
|
||||||
|
|
||||||
|
|
||||||
class _PyAccessI32_Swap(PyAccess):
|
class _PyAccessI32_Swap(PyAccess):
|
||||||
""" I;32L/B access, with byteswapping conversion """
|
"""I;32L/B access, with byteswapping conversion"""
|
||||||
|
|
||||||
def _post_init(self, *args, **kwargs):
|
def _post_init(self, *args, **kwargs):
|
||||||
self.pixels = self.image32
|
self.pixels = self.image32
|
||||||
|
@ -293,7 +293,7 @@ class _PyAccessI32_Swap(PyAccess):
|
||||||
|
|
||||||
|
|
||||||
class _PyAccessF(PyAccess):
|
class _PyAccessF(PyAccess):
|
||||||
""" 32 bit float access """
|
"""32 bit float access"""
|
||||||
|
|
||||||
def _post_init(self, *args, **kwargs):
|
def _post_init(self, *args, **kwargs):
|
||||||
self.pixels = ffi.cast("float **", self.image32)
|
self.pixels = ffi.cast("float **", self.image32)
|
||||||
|
|
|
@ -193,7 +193,8 @@ def _save(im, fp, filename):
|
||||||
for channel in im.split():
|
for channel in im.split():
|
||||||
fp.write(channel.tobytes("raw", rawmode, 0, orientation))
|
fp.write(channel.tobytes("raw", rawmode, 0, orientation))
|
||||||
|
|
||||||
fp.close()
|
if hasattr(fp, "flush"):
|
||||||
|
fp.flush()
|
||||||
|
|
||||||
|
|
||||||
class SGI16Decoder(ImageFile.PyDecoder):
|
class SGI16Decoder(ImageFile.PyDecoder):
|
||||||
|
|
|
@ -48,7 +48,7 @@ from collections.abc import MutableMapping
|
||||||
from fractions import Fraction
|
from fractions import Fraction
|
||||||
from numbers import Number, Rational
|
from numbers import Number, Rational
|
||||||
|
|
||||||
from . import Image, ImageFile, ImagePalette, TiffTags
|
from . import Image, ImageFile, ImageOps, ImagePalette, TiffTags
|
||||||
from ._binary import o8
|
from ._binary import o8
|
||||||
from .TiffTags import TYPES
|
from .TiffTags import TYPES
|
||||||
|
|
||||||
|
@ -93,6 +93,7 @@ SUBIFD = 330
|
||||||
EXTRASAMPLES = 338
|
EXTRASAMPLES = 338
|
||||||
SAMPLEFORMAT = 339
|
SAMPLEFORMAT = 339
|
||||||
JPEGTABLES = 347
|
JPEGTABLES = 347
|
||||||
|
YCBCRSUBSAMPLING = 530
|
||||||
REFERENCEBLACKWHITE = 532
|
REFERENCEBLACKWHITE = 532
|
||||||
COPYRIGHT = 33432
|
COPYRIGHT = 33432
|
||||||
IPTC_NAA_CHUNK = 33723 # newsphoto properties
|
IPTC_NAA_CHUNK = 33723 # newsphoto properties
|
||||||
|
@ -1497,7 +1498,9 @@ def _save(im, fp, filename):
|
||||||
|
|
||||||
ifd = ImageFileDirectory_v2(prefix=prefix)
|
ifd = ImageFileDirectory_v2(prefix=prefix)
|
||||||
|
|
||||||
compression = im.encoderinfo.get("compression", im.info.get("compression"))
|
encoderinfo = im.encoderinfo
|
||||||
|
encoderconfig = im.encoderconfig
|
||||||
|
compression = encoderinfo.get("compression", im.info.get("compression"))
|
||||||
if compression is None:
|
if compression is None:
|
||||||
compression = "raw"
|
compression = "raw"
|
||||||
elif compression == "tiff_jpeg":
|
elif compression == "tiff_jpeg":
|
||||||
|
@ -1515,10 +1518,10 @@ def _save(im, fp, filename):
|
||||||
ifd[IMAGELENGTH] = im.size[1]
|
ifd[IMAGELENGTH] = im.size[1]
|
||||||
|
|
||||||
# write any arbitrary tags passed in as an ImageFileDirectory
|
# write any arbitrary tags passed in as an ImageFileDirectory
|
||||||
if "tiffinfo" in im.encoderinfo:
|
if "tiffinfo" in encoderinfo:
|
||||||
info = im.encoderinfo["tiffinfo"]
|
info = encoderinfo["tiffinfo"]
|
||||||
elif "exif" in im.encoderinfo:
|
elif "exif" in encoderinfo:
|
||||||
info = im.encoderinfo["exif"]
|
info = encoderinfo["exif"]
|
||||||
if isinstance(info, bytes):
|
if isinstance(info, bytes):
|
||||||
exif = Image.Exif()
|
exif = Image.Exif()
|
||||||
exif.load(info)
|
exif.load(info)
|
||||||
|
@ -1556,7 +1559,7 @@ def _save(im, fp, filename):
|
||||||
|
|
||||||
# preserve ICC profile (should also work when saving other formats
|
# preserve ICC profile (should also work when saving other formats
|
||||||
# which support profiles as TIFF) -- 2008-06-06 Florian Hoech
|
# which support profiles as TIFF) -- 2008-06-06 Florian Hoech
|
||||||
icc = im.encoderinfo.get("icc_profile", im.info.get("icc_profile"))
|
icc = encoderinfo.get("icc_profile", im.info.get("icc_profile"))
|
||||||
if icc:
|
if icc:
|
||||||
ifd[ICCPROFILE] = icc
|
ifd[ICCPROFILE] = icc
|
||||||
|
|
||||||
|
@ -1572,10 +1575,10 @@ def _save(im, fp, filename):
|
||||||
(ARTIST, "artist"),
|
(ARTIST, "artist"),
|
||||||
(COPYRIGHT, "copyright"),
|
(COPYRIGHT, "copyright"),
|
||||||
]:
|
]:
|
||||||
if name in im.encoderinfo:
|
if name in encoderinfo:
|
||||||
ifd[key] = im.encoderinfo[name]
|
ifd[key] = encoderinfo[name]
|
||||||
|
|
||||||
dpi = im.encoderinfo.get("dpi")
|
dpi = encoderinfo.get("dpi")
|
||||||
if dpi:
|
if dpi:
|
||||||
ifd[RESOLUTION_UNIT] = 2
|
ifd[RESOLUTION_UNIT] = 2
|
||||||
ifd[X_RESOLUTION] = dpi[0]
|
ifd[X_RESOLUTION] = dpi[0]
|
||||||
|
@ -1590,7 +1593,18 @@ def _save(im, fp, filename):
|
||||||
if format != 1:
|
if format != 1:
|
||||||
ifd[SAMPLEFORMAT] = format
|
ifd[SAMPLEFORMAT] = format
|
||||||
|
|
||||||
|
if PHOTOMETRIC_INTERPRETATION not in ifd:
|
||||||
ifd[PHOTOMETRIC_INTERPRETATION] = photo
|
ifd[PHOTOMETRIC_INTERPRETATION] = photo
|
||||||
|
elif im.mode in ("1", "L") and ifd[PHOTOMETRIC_INTERPRETATION] == 0:
|
||||||
|
if im.mode == "1":
|
||||||
|
inverted_im = im.copy()
|
||||||
|
px = inverted_im.load()
|
||||||
|
for y in range(inverted_im.height):
|
||||||
|
for x in range(inverted_im.width):
|
||||||
|
px[x, y] = 0 if px[x, y] == 255 else 255
|
||||||
|
im = inverted_im
|
||||||
|
else:
|
||||||
|
im = ImageOps.invert(im)
|
||||||
|
|
||||||
if im.mode in ["P", "PA"]:
|
if im.mode in ["P", "PA"]:
|
||||||
lut = im.im.getpalette("RGB", "RGB;L")
|
lut = im.im.getpalette("RGB", "RGB;L")
|
||||||
|
@ -1600,6 +1614,9 @@ def _save(im, fp, filename):
|
||||||
# aim for 64 KB strips when using libtiff writer
|
# aim for 64 KB strips when using libtiff writer
|
||||||
if libtiff:
|
if libtiff:
|
||||||
rows_per_strip = min((2 ** 16 + stride - 1) // stride, im.size[1])
|
rows_per_strip = min((2 ** 16 + stride - 1) // stride, im.size[1])
|
||||||
|
# JPEG encoder expects multiple of 8 rows
|
||||||
|
if compression == "jpeg":
|
||||||
|
rows_per_strip = min(((rows_per_strip + 7) // 8) * 8, im.size[1])
|
||||||
else:
|
else:
|
||||||
rows_per_strip = im.size[1]
|
rows_per_strip = im.size[1]
|
||||||
strip_byte_counts = stride * rows_per_strip
|
strip_byte_counts = stride * rows_per_strip
|
||||||
|
@ -1616,9 +1633,16 @@ def _save(im, fp, filename):
|
||||||
# no compression by default:
|
# no compression by default:
|
||||||
ifd[COMPRESSION] = COMPRESSION_INFO_REV.get(compression, 1)
|
ifd[COMPRESSION] = COMPRESSION_INFO_REV.get(compression, 1)
|
||||||
|
|
||||||
|
if im.mode == "YCbCr":
|
||||||
|
for tag, value in {
|
||||||
|
YCBCRSUBSAMPLING: (1, 1),
|
||||||
|
REFERENCEBLACKWHITE: (0, 255, 128, 255, 128, 255),
|
||||||
|
}.items():
|
||||||
|
ifd.setdefault(tag, value)
|
||||||
|
|
||||||
if libtiff:
|
if libtiff:
|
||||||
if "quality" in im.encoderinfo:
|
if "quality" in encoderinfo:
|
||||||
quality = im.encoderinfo["quality"]
|
quality = encoderinfo["quality"]
|
||||||
if not isinstance(quality, int) or quality < 0 or quality > 100:
|
if not isinstance(quality, int) or quality < 0 or quality > 100:
|
||||||
raise ValueError("Invalid quality setting")
|
raise ValueError("Invalid quality setting")
|
||||||
if compression != "jpeg":
|
if compression != "jpeg":
|
||||||
|
@ -1707,7 +1731,7 @@ def _save(im, fp, filename):
|
||||||
tags = list(atts.items())
|
tags = list(atts.items())
|
||||||
tags.sort()
|
tags.sort()
|
||||||
a = (rawmode, compression, _fp, filename, tags, types)
|
a = (rawmode, compression, _fp, filename, tags, types)
|
||||||
e = Image._getencoder(im.mode, "libtiff", a, im.encoderconfig)
|
e = Image._getencoder(im.mode, "libtiff", a, encoderconfig)
|
||||||
e.setimage(im.im, (0, 0) + im.size)
|
e.setimage(im.im, (0, 0) + im.size)
|
||||||
while True:
|
while True:
|
||||||
# undone, change to self.decodermaxblock:
|
# undone, change to self.decodermaxblock:
|
||||||
|
@ -1727,7 +1751,7 @@ def _save(im, fp, filename):
|
||||||
)
|
)
|
||||||
|
|
||||||
# -- helper for multi-page save --
|
# -- helper for multi-page save --
|
||||||
if "_debug_multipage" in im.encoderinfo:
|
if "_debug_multipage" in encoderinfo:
|
||||||
# just to access o32 and o16 (using correct byte order)
|
# just to access o32 and o16 (using correct byte order)
|
||||||
im._debug_multipage = ifd
|
im._debug_multipage = ifd
|
||||||
|
|
||||||
|
|
|
@ -23,12 +23,44 @@ and has been tested with a few sample files found using google.
|
||||||
To open a WAL file, use the :py:func:`PIL.WalImageFile.open()` function instead.
|
To open a WAL file, use the :py:func:`PIL.WalImageFile.open()` function instead.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import builtins
|
from . import Image, ImageFile
|
||||||
|
|
||||||
from . import Image
|
|
||||||
from ._binary import i32le as i32
|
from ._binary import i32le as i32
|
||||||
|
|
||||||
|
|
||||||
|
class WalImageFile(ImageFile.ImageFile):
|
||||||
|
|
||||||
|
format = "WAL"
|
||||||
|
format_description = "Quake2 Texture"
|
||||||
|
|
||||||
|
def _open(self):
|
||||||
|
self.mode = "P"
|
||||||
|
|
||||||
|
# read header fields
|
||||||
|
header = self.fp.read(32 + 24 + 32 + 12)
|
||||||
|
self._size = i32(header, 32), i32(header, 36)
|
||||||
|
Image._decompression_bomb_check(self.size)
|
||||||
|
|
||||||
|
# load pixel data
|
||||||
|
offset = i32(header, 40)
|
||||||
|
self.fp.seek(offset)
|
||||||
|
|
||||||
|
# strings are null-terminated
|
||||||
|
self.info["name"] = header[:32].split(b"\0", 1)[0]
|
||||||
|
next_name = header[56 : 56 + 32].split(b"\0", 1)[0]
|
||||||
|
if next_name:
|
||||||
|
self.info["next_name"] = next_name
|
||||||
|
|
||||||
|
def load(self):
|
||||||
|
if self.im:
|
||||||
|
# Already loaded
|
||||||
|
return
|
||||||
|
|
||||||
|
self.im = Image.core.new(self.mode, self.size)
|
||||||
|
self.frombytes(self.fp.read(self.size[0] * self.size[1]))
|
||||||
|
self.putpalette(quake2palette)
|
||||||
|
Image.Image.load(self)
|
||||||
|
|
||||||
|
|
||||||
def open(filename):
|
def open(filename):
|
||||||
"""
|
"""
|
||||||
Load texture from a Quake2 WAL texture file.
|
Load texture from a Quake2 WAL texture file.
|
||||||
|
@ -39,38 +71,7 @@ def open(filename):
|
||||||
:param filename: WAL file name, or an opened file handle.
|
:param filename: WAL file name, or an opened file handle.
|
||||||
:returns: An image instance.
|
:returns: An image instance.
|
||||||
"""
|
"""
|
||||||
# FIXME: modify to return a WalImageFile instance instead of
|
return WalImageFile(filename)
|
||||||
# plain Image object ?
|
|
||||||
|
|
||||||
def imopen(fp):
|
|
||||||
# read header fields
|
|
||||||
header = fp.read(32 + 24 + 32 + 12)
|
|
||||||
size = i32(header, 32), i32(header, 36)
|
|
||||||
offset = i32(header, 40)
|
|
||||||
|
|
||||||
# load pixel data
|
|
||||||
fp.seek(offset)
|
|
||||||
|
|
||||||
Image._decompression_bomb_check(size)
|
|
||||||
im = Image.frombytes("P", size, fp.read(size[0] * size[1]))
|
|
||||||
im.putpalette(quake2palette)
|
|
||||||
|
|
||||||
im.format = "WAL"
|
|
||||||
im.format_description = "Quake2 Texture"
|
|
||||||
|
|
||||||
# strings are null-terminated
|
|
||||||
im.info["name"] = header[:32].split(b"\0", 1)[0]
|
|
||||||
next_name = header[56 : 56 + 32].split(b"\0", 1)[0]
|
|
||||||
if next_name:
|
|
||||||
im.info["next_name"] = next_name
|
|
||||||
|
|
||||||
return im
|
|
||||||
|
|
||||||
if hasattr(filename, "read"):
|
|
||||||
return imopen(filename)
|
|
||||||
else:
|
|
||||||
with builtins.open(filename, "rb") as fp:
|
|
||||||
return imopen(fp)
|
|
||||||
|
|
||||||
|
|
||||||
quake2palette = (
|
quake2palette = (
|
||||||
|
|
|
@ -202,7 +202,7 @@ def _save_all(im, fp, filename):
|
||||||
lossless = im.encoderinfo.get("lossless", False)
|
lossless = im.encoderinfo.get("lossless", False)
|
||||||
quality = im.encoderinfo.get("quality", 80)
|
quality = im.encoderinfo.get("quality", 80)
|
||||||
method = im.encoderinfo.get("method", 0)
|
method = im.encoderinfo.get("method", 0)
|
||||||
icc_profile = im.encoderinfo.get("icc_profile", "")
|
icc_profile = im.encoderinfo.get("icc_profile") or ""
|
||||||
exif = im.encoderinfo.get("exif", "")
|
exif = im.encoderinfo.get("exif", "")
|
||||||
if isinstance(exif, Image.Exif):
|
if isinstance(exif, Image.Exif):
|
||||||
exif = exif.tobytes()
|
exif = exif.tobytes()
|
||||||
|
@ -309,7 +309,7 @@ def _save_all(im, fp, filename):
|
||||||
def _save(im, fp, filename):
|
def _save(im, fp, filename):
|
||||||
lossless = im.encoderinfo.get("lossless", False)
|
lossless = im.encoderinfo.get("lossless", False)
|
||||||
quality = im.encoderinfo.get("quality", 80)
|
quality = im.encoderinfo.get("quality", 80)
|
||||||
icc_profile = im.encoderinfo.get("icc_profile", "")
|
icc_profile = im.encoderinfo.get("icc_profile") or ""
|
||||||
exif = im.encoderinfo.get("exif", "")
|
exif = im.encoderinfo.get("exif", "")
|
||||||
if isinstance(exif, Image.Exif):
|
if isinstance(exif, Image.Exif):
|
||||||
exif = exif.tobytes()
|
exif = exif.tobytes()
|
||||||
|
|
|
@ -47,6 +47,16 @@ def si16le(c, o=0):
|
||||||
return unpack_from("<h", c, o)[0]
|
return unpack_from("<h", c, o)[0]
|
||||||
|
|
||||||
|
|
||||||
|
def si16be(c, o=0):
|
||||||
|
"""
|
||||||
|
Converts a 2-bytes (16 bits) string to a signed integer, big endian.
|
||||||
|
|
||||||
|
:param c: string containing bytes to convert
|
||||||
|
:param o: offset of bytes to convert in string
|
||||||
|
"""
|
||||||
|
return unpack_from(">h", c, o)[0]
|
||||||
|
|
||||||
|
|
||||||
def i32le(c, o=0):
|
def i32le(c, o=0):
|
||||||
"""
|
"""
|
||||||
Converts a 4-bytes (32 bits) string to an unsigned integer.
|
Converts a 4-bytes (32 bits) string to an unsigned integer.
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
/* Small excerpts from the Tcl / Tk 8.6 headers
|
/* Small excerpts from the Tcl / Tk 8.6 headers
|
||||||
*
|
*
|
||||||
* License terms copied from:
|
* License terms copied from:
|
||||||
* http://www.tcl.tk/software/tcltk/license.html
|
* https://www.tcl.tk/software/tcltk/license.html
|
||||||
* as of 20 May 2016.
|
* as of 20 May 2016.
|
||||||
*
|
*
|
||||||
* Copyright (c) 1987-1994 The Regents of the University of California.
|
* Copyright (c) 1987-1994 The Regents of the University of California.
|
||||||
|
|
|
@ -808,6 +808,12 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) {
|
||||||
av + stride * 2);
|
av + stride * 2);
|
||||||
free(av);
|
free(av);
|
||||||
}
|
}
|
||||||
|
} else if (key_int == TIFFTAG_YCBCRSUBSAMPLING) {
|
||||||
|
status = ImagingLibTiffSetField(
|
||||||
|
&encoder->state,
|
||||||
|
(ttag_t)key_int,
|
||||||
|
(UINT16)PyLong_AsLong(PyTuple_GetItem(value, 0)),
|
||||||
|
(UINT16)PyLong_AsLong(PyTuple_GetItem(value, 1)));
|
||||||
} else if (type == TIFF_SHORT) {
|
} else if (type == TIFF_SHORT) {
|
||||||
UINT16 *av;
|
UINT16 *av;
|
||||||
/* malloc check ok, calloc checks for overflow */
|
/* malloc check ok, calloc checks for overflow */
|
||||||
|
|
|
@ -11,7 +11,7 @@
|
||||||
|
|
||||||
#include "Imaging.h"
|
#include "Imaging.h"
|
||||||
|
|
||||||
/* use Tests/make_hash.py to calculate these values */
|
/* use make_hash.py from the pillow-scripts repository to calculate these values */
|
||||||
#define ACCESS_TABLE_SIZE 27
|
#define ACCESS_TABLE_SIZE 27
|
||||||
#define ACCESS_TABLE_HASH 3078
|
#define ACCESS_TABLE_HASH 3078
|
||||||
|
|
||||||
|
|
|
@ -46,7 +46,8 @@ ImagingFliDecode(Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t byt
|
||||||
ptr = buf;
|
ptr = buf;
|
||||||
|
|
||||||
framesize = I32(ptr);
|
framesize = I32(ptr);
|
||||||
if (framesize < I32(ptr)) {
|
// there can be one pad byte in the framesize
|
||||||
|
if (bytes + (bytes % 2) < framesize) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -223,8 +224,15 @@ ImagingFliDecode(Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t byt
|
||||||
break;
|
break;
|
||||||
case 16:
|
case 16:
|
||||||
/* COPY chunk */
|
/* COPY chunk */
|
||||||
if (state->xsize > bytes / state->ysize) {
|
if (INT32_MAX / state->xsize < state->ysize) {
|
||||||
|
/* Integer overflow, bail */
|
||||||
|
state->errcode = IMAGING_CODEC_OVERRUN;
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
/* Note, have to check Data + size, not just ptr + size) */
|
||||||
|
if (data + (state->xsize * state->ysize) > ptr + bytes) {
|
||||||
/* not enough data for frame */
|
/* not enough data for frame */
|
||||||
|
/* UNDONE Unclear that we're actually going to leave the buffer at the right place. */
|
||||||
return ptr - buf; /* bytes consumed */
|
return ptr - buf; /* bytes consumed */
|
||||||
}
|
}
|
||||||
for (y = 0; y < state->ysize; y++) {
|
for (y = 0; y < state->ysize; y++) {
|
||||||
|
|
|
@ -29,7 +29,7 @@
|
||||||
|
|
||||||
/* This is to work around a bug in GCC prior 4.9 in 64 bit mode.
|
/* This is to work around a bug in GCC prior 4.9 in 64 bit mode.
|
||||||
GCC generates code with partial dependency which is 3 times slower.
|
GCC generates code with partial dependency which is 3 times slower.
|
||||||
See: http://stackoverflow.com/a/26588074/253146 */
|
See: https://stackoverflow.com/a/26588074/253146 */
|
||||||
#if defined(__x86_64__) && defined(__SSE__) && !defined(__NO_INLINE__) && \
|
#if defined(__x86_64__) && defined(__SSE__) && !defined(__NO_INLINE__) && \
|
||||||
!defined(__clang__) && defined(GCC_VERSION) && (GCC_VERSION < 40900)
|
!defined(__clang__) && defined(GCC_VERSION) && (GCC_VERSION < 40900)
|
||||||
static float __attribute__((always_inline)) inline _i2f(int v) {
|
static float __attribute__((always_inline)) inline _i2f(int v) {
|
||||||
|
|
|
@ -417,9 +417,16 @@ fill_mask_L(
|
||||||
if (imOut->image8) {
|
if (imOut->image8) {
|
||||||
for (y = 0; y < ysize; y++) {
|
for (y = 0; y < ysize; y++) {
|
||||||
UINT8 *out = imOut->image8[y + dy] + dx;
|
UINT8 *out = imOut->image8[y + dy] + dx;
|
||||||
|
if (strncmp(imOut->mode, "I;16", 4) == 0) {
|
||||||
|
out += dx;
|
||||||
|
}
|
||||||
UINT8 *mask = imMask->image8[y + sy] + sx;
|
UINT8 *mask = imMask->image8[y + sy] + sx;
|
||||||
for (x = 0; x < xsize; x++) {
|
for (x = 0; x < xsize; x++) {
|
||||||
*out = BLEND(*mask, *out, ink[0], tmp1);
|
*out = BLEND(*mask, *out, ink[0], tmp1);
|
||||||
|
if (strncmp(imOut->mode, "I;16", 4) == 0) {
|
||||||
|
out++;
|
||||||
|
*out = BLEND(*mask, *out, ink[0], tmp1);
|
||||||
|
}
|
||||||
out++, mask++;
|
out++, mask++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
4
src/thirdparty/fribidi-shim/fribidi.c
vendored
|
@ -12,7 +12,7 @@
|
||||||
|
|
||||||
|
|
||||||
/* FriBiDi>=1.0.0 adds bracket_types param, ignore and call legacy function */
|
/* FriBiDi>=1.0.0 adds bracket_types param, ignore and call legacy function */
|
||||||
FriBidiLevel fribidi_get_par_embedding_levels_ex_compat(
|
static FriBidiLevel fribidi_get_par_embedding_levels_ex_compat(
|
||||||
const FriBidiCharType *bidi_types,
|
const FriBidiCharType *bidi_types,
|
||||||
const FriBidiBracketType *bracket_types,
|
const FriBidiBracketType *bracket_types,
|
||||||
const FriBidiStrIndex len,
|
const FriBidiStrIndex len,
|
||||||
|
@ -24,7 +24,7 @@ FriBidiLevel fribidi_get_par_embedding_levels_ex_compat(
|
||||||
}
|
}
|
||||||
|
|
||||||
/* FriBiDi>=1.0.0 gets bracket types here, ignore */
|
/* FriBiDi>=1.0.0 gets bracket types here, ignore */
|
||||||
void fribidi_get_bracket_types_compat(
|
static void fribidi_get_bracket_types_compat(
|
||||||
const FriBidiChar *str,
|
const FriBidiChar *str,
|
||||||
const FriBidiStrIndex len,
|
const FriBidiStrIndex len,
|
||||||
const FriBidiCharType *types,
|
const FriBidiCharType *types,
|
||||||
|
|
4
src/thirdparty/fribidi-shim/fribidi.h
vendored
|
@ -63,8 +63,12 @@ typedef uint32_t FriBidiParType;
|
||||||
/* functions */
|
/* functions */
|
||||||
|
|
||||||
#ifdef FRIBIDI_SHIM_IMPLEMENTATION
|
#ifdef FRIBIDI_SHIM_IMPLEMENTATION
|
||||||
|
#ifdef _MSC_VER
|
||||||
#define FRIBIDI_ENTRY
|
#define FRIBIDI_ENTRY
|
||||||
#else
|
#else
|
||||||
|
#define FRIBIDI_ENTRY __attribute__((visibility ("hidden")))
|
||||||
|
#endif
|
||||||
|
#else
|
||||||
#define FRIBIDI_ENTRY extern
|
#define FRIBIDI_ENTRY extern
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
|
2
src/thirdparty/raqm/raqm.c
vendored
|
@ -491,7 +491,7 @@ raqm_set_text_utf8 (raqm_t *rq,
|
||||||
*
|
*
|
||||||
* The default is #RAQM_DIRECTION_DEFAULT, which determines the paragraph
|
* The default is #RAQM_DIRECTION_DEFAULT, which determines the paragraph
|
||||||
* direction based on the first character with strong bidi type (see [rule
|
* direction based on the first character with strong bidi type (see [rule
|
||||||
* P2](http://unicode.org/reports/tr9/#P2) in Unicode Bidirectional Algorithm),
|
* P2](https://unicode.org/reports/tr9/#P2) in Unicode Bidirectional Algorithm),
|
||||||
* which can be good enough for many cases but has problems when a mainly
|
* which can be good enough for many cases but has problems when a mainly
|
||||||
* right-to-left paragraph starts with a left-to-right character and vice versa
|
* right-to-left paragraph starts with a left-to-right character and vice versa
|
||||||
* as the detected paragraph direction will be the wrong one, or when text does
|
* as the detected paragraph direction will be the wrong one, or when text does
|
||||||
|
|
|
@ -105,9 +105,9 @@ header = [
|
||||||
# dependencies, listed in order of compilation
|
# dependencies, listed in order of compilation
|
||||||
deps = {
|
deps = {
|
||||||
"libjpeg": {
|
"libjpeg": {
|
||||||
"url": SF_MIRROR + "/project/libjpeg-turbo/2.1.0/libjpeg-turbo-2.1.0.tar.gz",
|
"url": SF_MIRROR + "/project/libjpeg-turbo/2.1.1/libjpeg-turbo-2.1.1.tar.gz",
|
||||||
"filename": "libjpeg-turbo-2.1.0.tar.gz",
|
"filename": "libjpeg-turbo-2.1.1.tar.gz",
|
||||||
"dir": "libjpeg-turbo-2.1.0",
|
"dir": "libjpeg-turbo-2.1.1",
|
||||||
"build": [
|
"build": [
|
||||||
cmd_cmake(
|
cmd_cmake(
|
||||||
[
|
[
|
||||||
|
@ -154,9 +154,9 @@ deps = {
|
||||||
# "bins": [r"libtiff\*.dll"],
|
# "bins": [r"libtiff\*.dll"],
|
||||||
},
|
},
|
||||||
"libwebp": {
|
"libwebp": {
|
||||||
"url": "http://downloads.webmproject.org/releases/webp/libwebp-1.2.0.tar.gz",
|
"url": "http://downloads.webmproject.org/releases/webp/libwebp-1.2.1.tar.gz",
|
||||||
"filename": "libwebp-1.2.0.tar.gz",
|
"filename": "libwebp-1.2.1.tar.gz",
|
||||||
"dir": "libwebp-1.2.0",
|
"dir": "libwebp-1.2.1",
|
||||||
"build": [
|
"build": [
|
||||||
cmd_rmdir(r"output\release-static"), # clean
|
cmd_rmdir(r"output\release-static"), # clean
|
||||||
cmd_nmake(
|
cmd_nmake(
|
||||||
|
@ -277,9 +277,9 @@ deps = {
|
||||||
"libs": [r"*.lib"],
|
"libs": [r"*.lib"],
|
||||||
},
|
},
|
||||||
"harfbuzz": {
|
"harfbuzz": {
|
||||||
"url": "https://github.com/harfbuzz/harfbuzz/archive/2.8.1.zip",
|
"url": "https://github.com/harfbuzz/harfbuzz/archive/2.9.0.zip",
|
||||||
"filename": "harfbuzz-2.8.1.zip",
|
"filename": "harfbuzz-2.9.0.zip",
|
||||||
"dir": "harfbuzz-2.8.1",
|
"dir": "harfbuzz-2.9.0",
|
||||||
"build": [
|
"build": [
|
||||||
cmd_cmake("-DHB_HAVE_FREETYPE:BOOL=TRUE"),
|
cmd_cmake("-DHB_HAVE_FREETYPE:BOOL=TRUE"),
|
||||||
cmd_nmake(target="clean"),
|
cmd_nmake(target="clean"),
|
||||||
|
|