Merge branch 'master' into tiff_exif

This commit is contained in:
Andrew Murray 2021-09-07 06:33:37 +10:00 committed by GitHub
commit 3f3828040b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
96 changed files with 1014 additions and 347 deletions

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 175 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 262 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 208 B

BIN
Tests/images/hopper_wal.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

BIN
Tests/images/zero_dpi.jp2 Normal file

Binary file not shown.

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

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

View File

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

View File

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

View File

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

View File

@ -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 = []

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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++;
} }
} }

View File

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

View File

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

View File

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

View File

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