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
python3 -m pip install --upgrade pip
python3 -m pip install --upgrade wheel
PYTHONOPTIMIZE=0 python3 -m pip install cffi
python3 -m pip install coverage
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 pyroma
python3 -m pip install test-image-results
# TODO Remove condition when numpy supports 3.10
if ! [ "$GHA_PYTHON_VERSION" == "3.10-dev" ]; then python3 -m pip install numpy ; fi
python3 -m pip install numpy
# PyQt5 doesn't support PyPy3
# Wheel doesn't yet support 3.10
if [[ $GHA_PYTHON_VERSION == 3.* && $GHA_PYTHON_VERSION != "3.10-dev" ]]; then
if [[ $GHA_PYTHON_VERSION == 3.* ]]; then
# arm64, ppc64le, s390x CPUs:
# "ERROR: Could not find a version that satisfies the requirement pyqt5"
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
echo -e "[openblas]\nlibraries = openblas\nlibrary_dirs = /usr/local/opt/openblas/lib" >> ~/.numpy-site.cfg
# TODO Remove condition when numpy supports 3.10
if ! [ "$GHA_PYTHON_VERSION" == "3.10-dev" ]; then python3 -m pip install numpy ; fi
python3 -m pip install numpy
# extra test images
pushd depends && ./install_extra_test_images.sh && popd

View File

@ -72,8 +72,6 @@ jobs:
if: startsWith(matrix.os, 'macOS')
run: |
.github/workflows/macos-install.sh
env:
GHA_PYTHON_VERSION: ${{ matrix.python-version }}
- name: Build
run: |

1
.gitignore vendored
View File

@ -83,6 +83,7 @@ docs/_build/
Tests/images/README.md
Tests/images/crash_1.tif
Tests/images/crash_2.tif
Tests/images/crash-81154a65438ba5aaeca73fd502fa4850fbde60f8.tif
Tests/images/string_dimension.tiff
Tests/images/jpeg2000
Tests/images/msp

View File

@ -1,6 +1,6 @@
repos:
- repo: https://github.com/psf/black
rev: e66be67b9b6811913470f70c28b4d50f94d05b22 # frozen: 20.8b1
rev: e3000ace2fd1fcb1c181bb7a8285f1f976bcbdc7 # frozen: 21.7b0
hooks:
- id: black
args: ["--target-version", "py36"]
@ -9,35 +9,38 @@ repos:
types: []
- repo: https://github.com/PyCQA/isort
rev: 377d260ffa6f746693f97b46d95025afc4bd8275 # frozen: 5.4.2
rev: fd5ba70665a37ec301a1f714ed09336048b3be63 # frozen: 5.9.3
hooks:
- id: isort
- repo: https://github.com/asottile/yesqa
rev: 7a009f3ee493c796827ee334f9058b110a0e0db8 # frozen: v1.2.1
rev: 644ede78511c02fc6f8e03e014cc1ddcfbf1e1f5 # frozen: v1.2.3
hooks:
- id: yesqa
- repo: https://github.com/Lucas-C/pre-commit-hooks
rev: f30f4974a08a6b2f6a1eeaf30a4d501cf909163a # frozen: v1.1.9
rev: 3592548bbd98528887eeed63486cf6c9bae00b98 # frozen: v1.1.10
hooks:
- id: remove-tabs
exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.opt$)
- repo: https://gitlab.com/pycqa/flake8
rev: 05f6544aef321e2fee03a1277ce2eef8880fb927 # frozen: 3.8.3
rev: dcd740bc0ebaf2b3d43e59a0060d157c97de13f3 # frozen: 3.9.2
hooks:
- id: flake8
additional_dependencies: [flake8-2020, flake8-implicit-str-concat]
- repo: https://github.com/pre-commit/pygrep-hooks
rev: eae6397e4c259ed3d057511f6dd5330b92867e62 # frozen: v1.6.0
rev: 6f51a66bba59954917140ec2eeeaa4d5e630e6ce # frozen: v1.9.0
hooks:
- id: python-check-blanket-noqa
- id: rst-backticks
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: e1668fe86af3810fbca72b8653fe478e66a0afdc # frozen: v3.2.0
rev: 38b88246ccc552bffaaf54259d064beeee434539 # frozen: v4.0.1
hooks:
- id: check-merge-conflict
- id: check-yaml
ci:
autoupdate_schedule: quarterly

View File

@ -2,6 +2,114 @@
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)
------------------

View File

@ -33,7 +33,7 @@ def _write_png(tmp_path, xdim, ydim):
def test_large(tmp_path):
""" succeeded prepatch"""
"""succeeded prepatch"""
_write_png(tmp_path, XDIM, YDIM)

View File

@ -31,7 +31,7 @@ def _write_png(tmp_path, xdim, ydim):
def test_large(tmp_path):
""" succeeded prepatch"""
"""succeeded prepatch"""
_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
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:
im.load()
assert im.n_frames == 1
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):
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((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):
test_file = str(tmp_path / "temp.png")
@ -609,3 +628,10 @@ def test_apng_save_blend(tmp_path):
im.seek(2)
assert im.getpixel((0, 0)) == (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():
""" Check a short header"""
"""Check a short header"""
with open(TEST_FILE_DXT5, "rb") as f:
img_file = f.read()
@ -210,7 +210,7 @@ def test_short_header():
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:
img_file = f.read()
@ -224,7 +224,7 @@ def test_short_file():
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:
px = im.getpixel((0, 0))

View File

@ -96,6 +96,17 @@ def test_showpage():
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")
def test_file_object(tmp_path):
# issue 479

View File

@ -138,3 +138,16 @@ def test_timeouts(test_file):
with Image.open(f) as im:
with pytest.raises(OSError):
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)
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):
# Pass in a different palette, as an ImagePalette.ImagePalette
# 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:
im.putpalette(palette)
assert_image_equal(reloaded, im)
assert_image_equal(reloaded.convert("RGB"), im.convert("RGB"))
def test_save_I(tmp_path):

View File

@ -18,6 +18,11 @@ def test_sanity():
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():
with Image.open("Tests/images/black_and_white.ico") as im:
assert im.mode == "RGBA"

View File

@ -630,7 +630,7 @@ class TestFileJpeg:
reloaded.save(f, quality="keep", optimize=True)
def test_bad_mpo_header(self):
""" Treat unknown MPO as JPEG """
"""Treat unknown MPO as JPEG"""
# Arrange
# Act
@ -718,6 +718,15 @@ class TestFileJpeg:
# This should return the default, and not raise a ZeroDivisionError
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):
# Arrange
# This is photoshop-200dpi.jpg with resolution removed from EXIF:

View File

@ -4,7 +4,7 @@ from io import BytesIO
import pytest
from PIL import Image, ImageFile, Jpeg2KImagePlugin, features
from PIL import Image, ImageFile, Jpeg2KImagePlugin, UnidentifiedImageError, features
from .helper import (
assert_image_equal,
@ -151,6 +151,38 @@ def test_reduce():
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):
outfile = str(tmp_path / "temp_layers.jp2")
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)
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:
assert_image_equal_tofile(png, "Tests/images/hopper_g4_500.tif")
# see https://github.com/python-pillow/Pillow/issues/279
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:
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")
def test_write_metadata(self, tmp_path):
""" Test metadata writing through libtiff """
"""Test metadata writing through libtiff"""
for legacy_api in [False, True]:
f = str(tmp_path / "temp.tiff")
with Image.open("Tests/images/hopper_g4.tif") as img:
@ -670,6 +670,15 @@ class TestFileLibTiff(LibTiffTestCase):
TiffImagePlugin.WRITE_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):
# issue 1597
with Image.open("Tests/images/rdf.tif") as im:
@ -968,10 +977,11 @@ class TestFileLibTiff(LibTiffTestCase):
assert str(e.value) == "-9"
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))
out = str(tmp_path / "temp.tif")
im.save(out, compression="tiff_adobe_deflate")
im.save(out, compression=compression)
with Image.open(out) as im:
# Assert that there are multiple strips

View File

@ -57,9 +57,10 @@ def test_n_frames():
assert im.n_frames == 1
assert not im.is_animated
with Image.open(test_file) as im:
assert im.n_frames == 2
assert im.is_animated
for path in [test_file, "Tests/images/negative_layer_count.psd"]:
with Image.open(path) as im:
assert im.n_frames == 2
assert im.is_animated
def test_eoferror():

View File

@ -73,6 +73,13 @@ def test_write(tmp_path):
img.save(out, format="sgi")
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"):
roundtrip(hopper(mode))

View File

@ -463,6 +463,15 @@ class TestFileTiff:
im.seek(1)
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):
filename = "Tests/images/pil136.tiff"
with Image.open(filename) as im:
@ -705,6 +714,8 @@ class TestFileTiff:
# Ignore this UserWarning which triggers for four tags:
# "Possibly corrupt EXIF data. Expecting to read 50404352 bytes but..."
@pytest.mark.filterwarnings("ignore:Possibly corrupt EXIF data")
# Ignore this UserWarning:
@pytest.mark.filterwarnings("ignore:Truncated File Read")
@pytest.mark.skipif(
not os.path.exists("Tests/images/string_dimension.tiff"),
reason="Extra image files not installed",

View File

@ -122,7 +122,7 @@ def test_read_metadata():
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:
f = str(tmp_path / "temp.tiff")
img.save(f, tiffinfo=img.tag)

View File

@ -1,15 +1,21 @@
from PIL import WalImageFile
from .helper import assert_image_equal_tofile
def test_open():
# Arrange
TEST_FILE = "Tests/images/hopper.wal"
# Act
im = WalImageFile.open(TEST_FILE)
with WalImageFile.open(TEST_FILE) as im:
# Assert
assert im.format == "WAL"
assert im.format_description == "Quake2 Texture"
assert im.mode == "P"
assert im.size == (128, 128)
# Assert
assert im.format == "WAL"
assert im.format_description == "Quake2 Texture"
assert im.mode == "P"
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)
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):
"""
Saving a black-and-white file to WebP format should work, and be

View File

@ -149,10 +149,11 @@ class TestImage:
assert im.mode == "RGB"
assert im.size == (128, 128)
temp_file = str(tmp_path / "temp.jpg")
if os.path.exists(temp_file):
os.remove(temp_file)
im.save(Path(temp_file))
for ext in (".jpg", ".jp2"):
temp_file = str(tmp_path / ("temp." + ext))
if os.path.exists(temp_file):
os.remove(temp_file)
im.save(Path(temp_file))
def test_fp_name(self, tmp_path):
temp_file = str(tmp_path / "temp.jpg")

View File

@ -14,6 +14,10 @@ def test_toarray():
ai = numpy.array(im.convert(mode))
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("L") == ((100, 128), "|u1", 12800)
@ -27,6 +31,9 @@ def test_toarray():
assert test("RGBA") == ((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 pytest.raises(OSError):
numpy.array(im_truncated)
@ -34,7 +41,7 @@ def test_toarray():
def test_fromarray():
class Wrapper:
""" Class with API matching Image.fromarray """
"""Class with API matching Image.fromarray"""
def __init__(self, img, arr_params):
self.img = img

View File

@ -42,10 +42,14 @@ def test_default():
im = hopper("P")
assert_image(im, "P", im.size)
im = im.convert()
assert_image(im, "RGB", im.size)
im = im.convert()
assert_image(im, "RGB", im.size)
converted_im = im.convert()
assert_image(converted_im, "RGB", im.size)
converted_im = im.convert()
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
@ -100,18 +104,22 @@ def test_trns_p(tmp_path):
# 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
im = hopper("P")
im.info["transparency"] = 128
# Act
im_rgba = im.convert("RGBA")
converted_im = im.convert(mode)
# Assert
assert "transparency" not in im_rgba.info
# https://github.com/python-pillow/Pillow/issues/2702
assert im_rgba.palette is None
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
assert converted_im.palette is None
def test_trns_l(tmp_path):

View File

@ -32,7 +32,7 @@ def test_16bit_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")
lut = [0.5 * float(x) for x in range(256)]

View File

@ -2,7 +2,7 @@ import pytest
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():
@ -36,9 +36,15 @@ def test_putpalette():
def test_imagepalette():
im = hopper("P")
im.putpalette(ImagePalette.negative())
assert_image_equal_tofile(im.convert("RGB"), "Tests/images/palette_negative.png")
im.putpalette(ImagePalette.random())
im.putpalette(ImagePalette.sepia())
assert_image_equal_tofile(im.convert("RGB"), "Tests/images/palette_sepia.png")
im.putpalette(ImagePalette.wedge())
assert_image_equal_tofile(im.convert("RGB"), "Tests/images/palette_wedge.png")
def test_putpalette_with_alpha_values():

View File

@ -63,6 +63,7 @@ def test_quantize_no_dither():
converted = image.quantize(dither=0, palette=palette)
assert_image(converted, "P", converted.size)
assert converted.palette.palette == palette.palette.palette
def test_quantize_dither_diff():

View File

@ -33,6 +33,9 @@ def test_angle():
with Image.open("Tests/images/test-card.png") as im:
rotate(im, im.mode, angle)
im = hopper()
assert_image_equal(im.rotate(angle), im.rotate(angle, expand=1))
def test_zero():
for angle in (0, 45, 90, 180, 270):

View File

@ -32,6 +32,11 @@ class TestImageTransform:
new_im = im.transform((100, 100), transform)
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):
im = hopper("RGB")
(w, h) = im.size

View File

@ -191,3 +191,12 @@ def test_rounding_errors():
assert (255, 255) == ImageColor.getcolor("white", "LA")
assert (163, 33) == ImageColor.getcolor("rgba(0, 255, 115, 33)", "LA")
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"
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):
im = Image.new(mode="RGB", size=(300, 100))
draw = ImageDraw.Draw(im)

View File

@ -156,23 +156,31 @@ def test_scale():
assert newimg.size == (25, 25)
def test_expand_palette():
im = Image.open("Tests/images/p_16.tga")
im_expanded = ImageOps.expand(im, 10, (255, 0, 0))
@pytest.mark.parametrize("border", (10, (1, 2, 3, 4)))
def test_expand_palette(border):
with Image.open("Tests/images/p_16.tga") as im:
im_expanded = ImageOps.expand(im, border, (255, 0, 0))
px = im_expanded.convert("RGB").load()
for b in range(10):
if isinstance(border, int):
left = top = right = bottom = border
else:
left, top, right, bottom = border
px = im_expanded.convert("RGB").load()
for x in range(im_expanded.width):
assert px[x, b] == (255, 0, 0)
assert px[x, im_expanded.height - 1 - b] == (255, 0, 0)
for b in range(top):
assert px[x, b] == (255, 0, 0)
for b in range(bottom):
assert px[x, im_expanded.height - 1 - b] == (255, 0, 0)
for y in range(im_expanded.height):
assert px[b, x] == (255, 0, 0)
assert px[b, im_expanded.width - 1 - b] == (255, 0, 0)
for b in range(left):
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(
(10, 10, im_expanded.width - 10, im_expanded.height - 10)
)
assert_image_equal(im_cropped, im)
im_cropped = im_expanded.crop(
(left, top, im_expanded.width - right, im_expanded.height - bottom)
)
assert_image_equal(im_cropped, im)
def test_colorize_2color():
@ -335,6 +343,28 @@ def test_exif_transpose():
) as 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():
# Test the cutoff argument of autocontrast

View File

@ -10,15 +10,16 @@ def test_sanity():
palette = ImagePalette.ImagePalette("RGB", list(range(256)) * 3)
assert len(palette.colors) == 256
with pytest.raises(ValueError):
ImagePalette.ImagePalette("RGB", list(range(256)) * 3, 10)
with pytest.warns(DeprecationWarning):
with pytest.raises(ValueError):
ImagePalette.ImagePalette("RGB", list(range(256)) * 3, 10)
def test_reload():
im = Image.open("Tests/images/hopper.gif")
original = im.copy()
im.palette.dirty = 1
assert_image_equal(im.convert("RGB"), original.convert("RGB"))
with Image.open("Tests/images/hopper.gif") as im:
original = im.copy()
im.palette.dirty = 1
assert_image_equal(im.convert("RGB"), original.convert("RGB"))
def test_getcolor():

View File

@ -24,11 +24,17 @@ def test_overflow():
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
with Image.open("Tests/images/l2rgb_read.bmp") as im:
with pytest.raises((ValueError, MemoryError, OSError)):
im.tobytes()
Image.MAX_IMAGE_PIXELS = max_pixels
@pytest.mark.skipif(sys.maxsize <= 2 ** 32, reason="Requires 64-bit system")
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:Metadata warning")
@pytest.mark.filterwarnings("ignore:Truncated File Read")
def test_tiff_crashes(test_file):
try:
with Image.open(test_file) as im:

View File

@ -1,7 +1,7 @@
#!/bin/bash
# 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

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

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.
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**
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.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
^^^
@ -839,7 +844,7 @@ Reading Multi-frame TIFF Images
The TIFF loader supports the :py:meth:`~PIL.Image.Image.seek` and
: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
(``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.
``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 |
+======================+=====+=====+=====+=====+=====+=====+=====+=====+
| 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 | | |
+----------------------+-----+-----+-----+-----+-----+-----+-----+-----+
@ -111,7 +111,7 @@ Pillow can be installed on FreeBSD via the official Ports or Packages systems:
**Packages**::
pkg install py36-pillow
pkg install py38-pillow
.. note::
@ -476,7 +476,7 @@ These platforms are built and tested for every change.
| +---------------------------+---------------------+
| | 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 |
| | | 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 | |
+----------------------------------+---------------------------+------------------+--------------+

View File

@ -9,10 +9,6 @@ represent the color palette of palette mapped images.
.. 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,
but they are all marked as "experimental." Read that as you will. The
``[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
GNU gdb (Ubuntu 9.2-0ubuntu1~20.04) 9.2
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.
There is NO WARRANTY, to the extent permitted by law.
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:
<http://www.gnu.org/software/gdb/bugs/>.
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".
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
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
image a ``ValueError`` will be raised.
image a :py:exc:`ValueError` will be raised.
Individual text chunks are limited to
: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::
:maxdepth: 2
8.4.0
8.3.2
8.3.1
8.3.0
8.2.0
8.1.2

View File

@ -533,14 +533,16 @@ class pil_build_ext(build_ext):
_add_directory(include_dirs, "/usr/X11/include")
# SDK install path
try:
sdk_path = (
subprocess.check_output(["xcrun", "--show-sdk-path"])
.strip()
.decode("latin1")
)
except Exception:
sdk_path = None
sdk_path = "/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk"
if not os.path.exists(sdk_path):
try:
sdk_path = (
subprocess.check_output(["xcrun", "--show-sdk-path"])
.strip()
.decode("latin1")
)
except Exception:
sdk_path = None
if sdk_path:
_add_directory(library_dirs, os.path.join(sdk_path, "usr", "lib"))
_add_directory(include_dirs, os.path.join(sdk_path, "usr", "include"))

View File

@ -58,7 +58,7 @@ def _dib_accept(prefix):
# Image plugin for the Windows BMP format.
# =============================================================================
class BmpImageFile(ImageFile.ImageFile):
""" Image plugin for the Windows Bitmap format (BMP) """
"""Image plugin for the Windows Bitmap format (BMP)"""
# ------------------------------------------------------------- Description
format_description = "Windows Bitmap"
@ -70,7 +70,7 @@ class BmpImageFile(ImageFile.ImageFile):
vars()[k] = v
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
if header:
seek(header)
@ -257,7 +257,7 @@ class BmpImageFile(ImageFile.ImageFile):
]
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
head_data = self.fp.read(14)
# choke if the file does not have the required magic bytes

View File

@ -61,7 +61,7 @@ def has_ghostscript():
return False
def Ghostscript(tile, size, fp, scale=1):
def Ghostscript(tile, size, fp, scale=1, transparency=False):
"""Render an image using Ghostscript"""
# Unpack decoder tile
@ -108,6 +108,8 @@ def Ghostscript(tile, size, fp, scale=1):
lengthfile -= len(s)
f.write(s)
device = "pngalpha" if transparency else "ppmraw"
# Build Ghostscript command
command = [
"gs",
@ -117,7 +119,7 @@ def Ghostscript(tile, size, fp, scale=1):
"-dBATCH", # exit after processing
"-dNOPAUSE", # don't pause between pages
"-dSAFER", # safe mode
"-sDEVICE=ppmraw", # ppm driver
f"-sDEVICE={device}",
f"-sOutputFile={outfile}", # output file
# adjust for image origin
"-c",
@ -325,11 +327,11 @@ class EpsImageFile(ImageFile.ImageFile):
return (length, offset)
def load(self, scale=1):
def load(self, scale=1, transparency=False):
# Load EPS via Ghostscript
if not self.tile:
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._size = self.im.size
self.tile = []

View File

@ -396,15 +396,7 @@ def _normalize_palette(im, palette, info):
if isinstance(palette, (bytes, bytearray, list)):
source_palette = bytearray(palette[:768])
if isinstance(palette, ImagePalette.ImagePalette):
source_palette = bytearray(
itertools.chain.from_iterable(
zip(
palette.palette[:256],
palette.palette[256:512],
palette.palette[512:768],
)
)
)
source_palette = bytearray(palette.palette)
if im.mode == "P":
if not source_palette:
@ -414,9 +406,26 @@ def _normalize_palette(im, palette, info):
source_palette = bytearray(i // 3 for i in range(768))
im.palette = ImagePalette.ImagePalette("RGB", palette=source_palette)
used_palette_colors = _get_optimize(im, info)
if used_palette_colors is not None:
return im.remap_palette(used_palette_colors, 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)
if used_palette_colors is not None:
return im.remap_palette(used_palette_colors, source_palette)
im.palette.palette = source_palette
return im
@ -507,7 +516,8 @@ def _write_multiple_frames(im, fp, palette):
offset = (0, 0)
else:
# compress difference
frame_data["encoderinfo"]["include_color_table"] = True
if not palette:
frame_data["encoderinfo"]["include_color_table"] = True
im_frame = im_frame.crop(frame_data["bbox"])
offset = frame_data["bbox"][:2]
@ -787,7 +797,7 @@ def _get_global_header(im, info):
"""Return a list of strings representing a GIF header"""
# 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"
for extensionKey in ["transparency", "duration", "loop", "comment"]:

View File

@ -235,8 +235,8 @@ class IcoFile:
# the total mask data is
# 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)
and_mask_offset = header["offset"] + header["size"] - total_bytes
self.buf.seek(and_mask_offset)
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
return b.getvalue()
def __array__(self):
def __array__(self, dtype=None):
# numpy array interface support
import numpy as np
@ -700,7 +700,7 @@ class Image:
class ArrayData:
__array_interface__ = new
return np.array(ArrayData())
return np.array(ArrayData(), dtype)
def __getstate__(self):
return [self.info, self.mode, self.size, self.getpalette(), self.tobytes()]
@ -914,16 +914,18 @@ class Image:
self.load()
has_transparency = self.info.get("transparency") is not None
if not mode and self.mode == "P":
# determine default mode
if self.palette:
mode = self.palette.mode
else:
mode = "RGB"
if mode == "RGB" and has_transparency:
mode = "RGBA"
if not mode or (mode == self.mode and not matrix):
return self.copy()
has_transparency = self.info.get("transparency") is not None
if matrix:
# matrix conversion
if mode not in ("L", "RGB"):
@ -1005,7 +1007,7 @@ class Image:
trns_im = trns_im.convert("RGB")
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"]
delete_trns = True
@ -1128,7 +1130,9 @@ class Image:
"only RGB or L mode images can be quantized to a palette"
)
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))
@ -1751,14 +1755,19 @@ class Image:
Attaches a palette to this image. The image must be a "P", "PA", "L"
or "LA" image.
The palette sequence must contain at most 768 integer values, or 1024
integer values if alpha is included. Each group of values represents
the red, green, blue (and alpha if included) values for the
corresponding pixel index. Instead of an integer sequence, you can use
an 8-bit string.
The palette sequence must contain at most 256 colors, made up of one
integer value for each channel in the raw mode.
For example, if the raw mode is "RGB", then it can contain at most 768
values, made up of red, green and blue values for the corresponding pixel
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 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
@ -1832,18 +1841,16 @@ class Image:
if source_palette is None:
if self.mode == "P":
self.load()
real_source_palette = self.im.getpalette("RGB")[:768]
source_palette = self.im.getpalette("RGB")[:768]
else: # L-mode
real_source_palette = bytearray(i // 3 for i in range(768))
else:
real_source_palette = source_palette
source_palette = bytearray(i // 3 for i in range(768))
palette_bytes = b""
new_positions = [0] * 256
# pick only the used colors from the palette
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
# replace the palette color id of all pixel with the new id
@ -2076,10 +2083,8 @@ class Image:
return self.copy()
if angle == 180:
return self.transpose(ROTATE_180)
if angle == 90 and expand:
return self.transpose(ROTATE_90)
if angle == 270 and expand:
return self.transpose(ROTATE_270)
if angle in (90, 270) and (expand or self.width == self.height):
return self.transpose(ROTATE_90 if angle == 90 else ROTATE_270)
# Calculate the affine matrix. Note that this is the reverse
# transformation (from destination image to source) because we
@ -2182,12 +2187,12 @@ class Image:
filename = ""
open_fp = False
if isPath(fp):
filename = fp
open_fp = True
elif isinstance(fp, Path):
if isinstance(fp, Path):
filename = str(fp)
open_fp = True
elif isPath(fp):
filename = fp
open_fp = True
elif fp == sys.stdout:
try:
fp = sys.stdout.buffer
@ -2481,6 +2486,8 @@ class Image:
raise ValueError("missing method data")
im = new(self.mode, size, fillcolor)
if self.mode == "P" and self.palette:
im.palette = self.palette.copy()
im.info = self.info.copy()
if method == MESH:
# list of quads

View File

@ -37,7 +37,7 @@ pyCMS
http://www.cazabon.com
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)
Originally released under LGPL. Graciously donated to PIL in

View File

@ -32,6 +32,8 @@ def getrgb(color):
:param color: A color string
:return: ``(red, green, blue[, alpha])``
"""
if len(color) > 100:
raise ValueError("color specifier is too long")
color = color.lower()
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
# a tricky case.
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()
return
try:

View File

@ -19,8 +19,9 @@
import functools
import operator
import re
from . import Image, ImageDraw
from . import Image
#
# helpers
@ -394,15 +395,16 @@ def expand(image, border=0, fill=0):
height = top + image.size[1] + bottom
color = _color(fill, image.mode)
if image.mode == "P" and image.palette:
out = Image.new(image.mode, (width, height))
out.putpalette(image.palette)
out.paste(image, (left, top))
draw = ImageDraw.Draw(out)
draw.rectangle((0, 0, width - 1, height - 1), outline=color, width=border)
image.load()
palette = image.palette.copy()
if isinstance(color, tuple):
color = palette.getcolor(color)
else:
out = Image.new(image.mode, (width, height), color)
out.paste(image, (left, top))
palette = None
out = Image.new(image.mode, (width, height), color)
if palette:
out.putpalette(palette.palette)
out.paste(image, (left, top))
return out
@ -588,7 +590,19 @@ def exif_transpose(image):
if method is not None:
transposed_image = image.transpose(method)
transposed_exif = transposed_image.getexif()
del transposed_exif[0x0112]
transposed_image.info["exif"] = transposed_exif.tobytes()
if 0x0112 in transposed_exif:
del transposed_exif[0x0112]
if "exif" in transposed_image.info:
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 image.copy()

View File

@ -17,6 +17,7 @@
#
import array
import warnings
from . import GimpGradientFile, GimpPaletteFile, ImageColor, PaletteFile
@ -25,15 +26,14 @@ class ImagePalette:
"""
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"
: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``
times the number of colors in ``mode``. The list must be aligned
by channel (All R values must be contiguous in the list before G
and B values.) Defaults to 0 through 255 per channel.
:param size: An optional palette size. If given, it cannot be equal to
or greater than 256. Defaults to 0.
an array or a list of ints between 0-255. The list must consist of
all channels for one color followed by the next color (e.g. RGBRGBRGB).
Defaults to an empty palette.
:param size: An optional palette size. If given, an error is raised
if ``palette`` is not of equal length.
"""
def __init__(self, mode="RGB", palette=None, size=0):
@ -41,8 +41,14 @@ class ImagePalette:
self.rawmode = None # if set, palette contains raw data
self.palette = palette or bytearray()
self.dirty = None
if size != 0 and size != len(self.palette):
raise ValueError("wrong palette size")
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")
@property
def palette(self):
@ -205,9 +211,9 @@ def make_gamma_lut(exp):
def negative(mode="RGB"):
palette = list(range(256))
palette = list(range(256 * len(mode)))
palette.reverse()
return ImagePalette(mode, palette * len(mode))
return ImagePalette(mode, [i // len(mode) for i in palette])
def random(mode="RGB"):
@ -220,15 +226,13 @@ def random(mode="RGB"):
def sepia(white="#fff0c0"):
r, g, b = ImageColor.getrgb(white)
r = make_linear_lut(0, r)
g = make_linear_lut(0, g)
b = make_linear_lut(0, b)
return ImagePalette("RGB", r + g + b)
bands = [make_linear_lut(0, band) for band in ImageColor.getrgb(white)]
return ImagePalette("RGB", [bands[i % 3][i // 3] for i in range(256 * 3)])
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):

View File

@ -6,6 +6,7 @@
#
# History:
# 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 Alastair Houghton
@ -19,6 +20,79 @@ import struct
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):
"""Parse the JPEG 2000 codestream to extract the size and component
count from the SIZ marker segment, returning a PIL (size, mode) tuple."""
@ -53,101 +127,71 @@ def _parse_codestream(fp):
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):
"""Parse the JP2 header box to extract size, component count and
color space information, returning a (size, mode, mimetype) tuple."""
"""Parse the JP2 header box to extract size, component count,
color space information, and optionally DPI information,
returning a (size, mode, mimetype, dpi) tuple."""
# Find the JP2 header box
reader = BoxReader(fp)
header = None
mimetype = None
while True:
lbox, tbox = struct.unpack(">I4s", fp.read(8))
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")
while reader.has_next_box():
tbox = reader.next_box_type()
if tbox == b"jp2h":
header = fp.read(lbox - hlen)
header = reader.read_boxes()
break
elif tbox == b"ftyp":
if fp.read(4) == b"jpx ":
if reader.read_fields(">4s")[0] == b"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
mode = None
bpc = None
nc = None
dpi = None # 2-tuple of DPI info, or None
hio = io.BytesIO(header)
while True:
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)
while header.has_next_box():
tbox = header.next_box_type()
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)
if unkc:
if nc == 1 and (bpc & 0x7F) > 8:
mode = "I;16"
elif nc == 1:
mode = "L"
elif nc == 2:
mode = "LA"
elif nc == 3:
mode = "RGB"
elif nc == 4:
mode = "RGBA"
break
elif tbox == b"colr":
meth, prec, approx = struct.unpack_from(">BBB", content)
if meth == 1:
cs = struct.unpack_from(">I", content, 3)[0]
if cs == 16: # sRGB
if nc == 1 and (bpc & 0x7F) > 8:
mode = "I;16"
elif nc == 1:
mode = "L"
elif nc == 3:
mode = "RGB"
elif nc == 4:
mode = "RGBA"
break
elif cs == 17: # grayscale
if nc == 1 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"
if nc == 1 and (bpc & 0x7F) > 8:
mode = "I;16"
elif nc == 1:
mode = "L"
elif nc == 2:
mode = "LA"
elif nc == 3:
mode = "RGB"
elif nc == 4:
mode = "RGBA"
elif tbox == b"res ":
res = header.read_boxes()
while res.has_next_box():
tres = res.next_box_type()
if tres == b"resc":
vrcn, vrcd, hrcn, hrcd, vrce, hrce = res.read_fields(">HHHHBB")
hres = _res_to_dpi(hrcn, hrcd, hrce)
vres = _res_to_dpi(vrcn, vrcd, vrce)
if hres is not None and vres is not None:
dpi = (hres, vres)
break
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":
self.codec = "jp2"
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:
raise SyntaxError("not a JPEG 2000 file")

View File

@ -168,11 +168,11 @@ def APP(self, marker):
# 1 dpcm = 2.54 dpi
dpi *= 2.54
self.info["dpi"] = dpi, dpi
except (KeyError, SyntaxError, ValueError, ZeroDivisionError):
except (TypeError, KeyError, SyntaxError, ValueError, ZeroDivisionError):
# SyntaxError for invalid/unreadable EXIF
# KeyError for dpi not included
# 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

View File

@ -21,7 +21,7 @@
# Figure 205. Windows Paint Version 1: "DanM" Format
# 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 struct
@ -73,7 +73,7 @@ class MspImageFile(ImageFile.ImageFile):
class MspDecoder(ImageFile.PyDecoder):
# 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
# Encyclopedia of Graphics File Formats and is licensed by
# 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"))
duration = im.encoderinfo.get("duration", im.info.get("duration", 0))
loop = im.encoderinfo.get("loop", im.info.get("loop", 0))
disposal = im.encoderinfo.get("disposal", im.info.get("disposal"))
blend = im.encoderinfo.get("blend", im.info.get("blend"))
disposal = im.encoderinfo.get(
"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:
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_blend == encoderinfo.get("blend")
):
duration = encoderinfo.get("duration", 0)
if duration:
if "duration" in previous["encoderinfo"]:
previous["encoderinfo"]["duration"] += duration
else:
previous["encoderinfo"]["duration"] = duration
if isinstance(duration, (list, tuple)):
previous["encoderinfo"]["duration"] += encoderinfo["duration"]
continue
else:
bbox = None
@ -1149,9 +1147,10 @@ def _write_multiple_frames(im, fp, chunk, rawmode):
bbox = frame_data["bbox"]
im_frame = im_frame.crop(bbox)
size = im_frame.size
duration = int(round(frame_data["encoderinfo"].get("duration", 0)))
disposal = frame_data["encoderinfo"].get("disposal", APNG_DISPOSE_OP_NONE)
blend = frame_data["encoderinfo"].get("blend", APNG_BLEND_OP_SOURCE)
encoderinfo = frame_data["encoderinfo"]
frame_duration = int(round(encoderinfo.get("duration", duration)))
frame_disposal = encoderinfo.get("disposal", disposal)
frame_blend = encoderinfo.get("blend", blend)
# frame control
chunk(
fp,
@ -1161,10 +1160,10 @@ def _write_multiple_frames(im, fp, chunk, rawmode):
o32(size[1]), # height
o32(bbox[0]), # x_offset
o32(bbox[1]), # y_offset
o16(duration), # delay_numerator
o16(frame_duration), # delay_numerator
o16(1000), # delay_denominator
o8(disposal), # dispose_op
o8(blend), # blend_op
o8(frame_disposal), # dispose_op
o8(frame_blend), # blend_op
)
seq_num += 1
# frame data

View File

@ -22,6 +22,7 @@ from . import Image, ImageFile, ImagePalette
from ._binary import i8
from ._binary import i16be as i16
from ._binary import i32be as i32
from ._binary import si16be as si16
MODES = {
# (photoshop mode, bits) -> (pil mode, required channels)
@ -179,7 +180,7 @@ def _layerinfo(fp, ct_bytes):
def read(size):
return ImageFile._safe_read(fp, size)
ct = i16(read(2))
ct = si16(read(2))
# sanity check
if ct_bytes < (abs(ct) * 20):

View File

@ -128,7 +128,7 @@ class 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):
self.pixels = ffi.cast("struct Pixel_RGBA **", self.image32)
@ -145,7 +145,7 @@ class _PyAccess32_2(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):
self.pixels = ffi.cast("struct Pixel_RGBA **", self.image32)
@ -164,7 +164,7 @@ class _PyAccess32_3(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):
self.pixels = ffi.cast("struct Pixel_RGBA **", self.image32)
@ -183,7 +183,7 @@ class _PyAccess32_4(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):
self.pixels = self.image8
@ -201,7 +201,7 @@ class _PyAccess8(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):
self.pixels = ffi.cast("unsigned short **", self.image)
@ -219,7 +219,7 @@ class _PyAccessI16_N(PyAccess):
class _PyAccessI16_L(PyAccess):
""" I;16L access, with conversion """
"""I;16L access, with conversion"""
def _post_init(self, *args, **kwargs):
self.pixels = ffi.cast("struct Pixel_I16 **", self.image)
@ -240,7 +240,7 @@ class _PyAccessI16_L(PyAccess):
class _PyAccessI16_B(PyAccess):
""" I;16B access, with conversion """
"""I;16B access, with conversion"""
def _post_init(self, *args, **kwargs):
self.pixels = ffi.cast("struct Pixel_I16 **", self.image)
@ -261,7 +261,7 @@ class _PyAccessI16_B(PyAccess):
class _PyAccessI32_N(PyAccess):
""" Signed Int32 access, native endian """
"""Signed Int32 access, native endian"""
def _post_init(self, *args, **kwargs):
self.pixels = self.image32
@ -274,7 +274,7 @@ class _PyAccessI32_N(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):
self.pixels = self.image32
@ -293,7 +293,7 @@ class _PyAccessI32_Swap(PyAccess):
class _PyAccessF(PyAccess):
""" 32 bit float access """
"""32 bit float access"""
def _post_init(self, *args, **kwargs):
self.pixels = ffi.cast("float **", self.image32)

View File

@ -193,7 +193,8 @@ def _save(im, fp, filename):
for channel in im.split():
fp.write(channel.tobytes("raw", rawmode, 0, orientation))
fp.close()
if hasattr(fp, "flush"):
fp.flush()
class SGI16Decoder(ImageFile.PyDecoder):

View File

@ -48,7 +48,7 @@ from collections.abc import MutableMapping
from fractions import Fraction
from numbers import Number, Rational
from . import Image, ImageFile, ImagePalette, TiffTags
from . import Image, ImageFile, ImageOps, ImagePalette, TiffTags
from ._binary import o8
from .TiffTags import TYPES
@ -93,6 +93,7 @@ SUBIFD = 330
EXTRASAMPLES = 338
SAMPLEFORMAT = 339
JPEGTABLES = 347
YCBCRSUBSAMPLING = 530
REFERENCEBLACKWHITE = 532
COPYRIGHT = 33432
IPTC_NAA_CHUNK = 33723 # newsphoto properties
@ -1497,7 +1498,9 @@ def _save(im, fp, filename):
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:
compression = "raw"
elif compression == "tiff_jpeg":
@ -1515,10 +1518,10 @@ def _save(im, fp, filename):
ifd[IMAGELENGTH] = im.size[1]
# write any arbitrary tags passed in as an ImageFileDirectory
if "tiffinfo" in im.encoderinfo:
info = im.encoderinfo["tiffinfo"]
elif "exif" in im.encoderinfo:
info = im.encoderinfo["exif"]
if "tiffinfo" in encoderinfo:
info = encoderinfo["tiffinfo"]
elif "exif" in encoderinfo:
info = encoderinfo["exif"]
if isinstance(info, bytes):
exif = Image.Exif()
exif.load(info)
@ -1556,7 +1559,7 @@ def _save(im, fp, filename):
# preserve ICC profile (should also work when saving other formats
# 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:
ifd[ICCPROFILE] = icc
@ -1572,10 +1575,10 @@ def _save(im, fp, filename):
(ARTIST, "artist"),
(COPYRIGHT, "copyright"),
]:
if name in im.encoderinfo:
ifd[key] = im.encoderinfo[name]
if name in encoderinfo:
ifd[key] = encoderinfo[name]
dpi = im.encoderinfo.get("dpi")
dpi = encoderinfo.get("dpi")
if dpi:
ifd[RESOLUTION_UNIT] = 2
ifd[X_RESOLUTION] = dpi[0]
@ -1590,7 +1593,18 @@ def _save(im, fp, filename):
if format != 1:
ifd[SAMPLEFORMAT] = format
ifd[PHOTOMETRIC_INTERPRETATION] = photo
if PHOTOMETRIC_INTERPRETATION not in ifd:
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"]:
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
if libtiff:
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:
rows_per_strip = im.size[1]
strip_byte_counts = stride * rows_per_strip
@ -1616,9 +1633,16 @@ def _save(im, fp, filename):
# no compression by default:
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 "quality" in im.encoderinfo:
quality = im.encoderinfo["quality"]
if "quality" in encoderinfo:
quality = encoderinfo["quality"]
if not isinstance(quality, int) or quality < 0 or quality > 100:
raise ValueError("Invalid quality setting")
if compression != "jpeg":
@ -1707,7 +1731,7 @@ def _save(im, fp, filename):
tags = list(atts.items())
tags.sort()
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)
while True:
# undone, change to self.decodermaxblock:
@ -1727,7 +1751,7 @@ def _save(im, fp, filename):
)
# -- 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)
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.
"""
import builtins
from . import Image
from . import Image, ImageFile
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):
"""
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.
:returns: An image instance.
"""
# FIXME: modify to return a WalImageFile instance instead of
# 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)
return WalImageFile(filename)
quake2palette = (

View File

@ -202,7 +202,7 @@ def _save_all(im, fp, filename):
lossless = im.encoderinfo.get("lossless", False)
quality = im.encoderinfo.get("quality", 80)
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", "")
if isinstance(exif, Image.Exif):
exif = exif.tobytes()
@ -309,7 +309,7 @@ def _save_all(im, fp, filename):
def _save(im, fp, filename):
lossless = im.encoderinfo.get("lossless", False)
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", "")
if isinstance(exif, Image.Exif):
exif = exif.tobytes()

View File

@ -47,6 +47,16 @@ def si16le(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):
"""
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
*
* 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.
*
* 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);
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) {
UINT16 *av;
/* malloc check ok, calloc checks for overflow */

View File

@ -11,7 +11,7 @@
#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_HASH 3078

View File

@ -46,7 +46,8 @@ ImagingFliDecode(Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t byt
ptr = buf;
framesize = I32(ptr);
if (framesize < I32(ptr)) {
// there can be one pad byte in the framesize
if (bytes + (bytes % 2) < framesize) {
return 0;
}
@ -223,8 +224,15 @@ ImagingFliDecode(Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t byt
break;
case 16:
/* 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 */
/* UNDONE Unclear that we're actually going to leave the buffer at the right place. */
return ptr - buf; /* bytes consumed */
}
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.
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__) && \
!defined(__clang__) && defined(GCC_VERSION) && (GCC_VERSION < 40900)
static float __attribute__((always_inline)) inline _i2f(int v) {

View File

@ -417,9 +417,16 @@ fill_mask_L(
if (imOut->image8) {
for (y = 0; y < ysize; y++) {
UINT8 *out = imOut->image8[y + dy] + dx;
if (strncmp(imOut->mode, "I;16", 4) == 0) {
out += dx;
}
UINT8 *mask = imMask->image8[y + sy] + sx;
for (x = 0; x < xsize; x++) {
*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++;
}
}

View File

@ -12,7 +12,7 @@
/* 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 FriBidiBracketType *bracket_types,
const FriBidiStrIndex len,
@ -24,7 +24,7 @@ FriBidiLevel fribidi_get_par_embedding_levels_ex_compat(
}
/* 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 FriBidiStrIndex len,
const FriBidiCharType *types,

View File

@ -63,8 +63,12 @@ typedef uint32_t FriBidiParType;
/* functions */
#ifdef FRIBIDI_SHIM_IMPLEMENTATION
#ifdef _MSC_VER
#define FRIBIDI_ENTRY
#else
#define FRIBIDI_ENTRY __attribute__((visibility ("hidden")))
#endif
#else
#define FRIBIDI_ENTRY extern
#endif

View File

@ -491,7 +491,7 @@ raqm_set_text_utf8 (raqm_t *rq,
*
* The default is #RAQM_DIRECTION_DEFAULT, which determines the paragraph
* 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
* 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

View File

@ -105,9 +105,9 @@ header = [
# dependencies, listed in order of compilation
deps = {
"libjpeg": {
"url": SF_MIRROR + "/project/libjpeg-turbo/2.1.0/libjpeg-turbo-2.1.0.tar.gz",
"filename": "libjpeg-turbo-2.1.0.tar.gz",
"dir": "libjpeg-turbo-2.1.0",
"url": SF_MIRROR + "/project/libjpeg-turbo/2.1.1/libjpeg-turbo-2.1.1.tar.gz",
"filename": "libjpeg-turbo-2.1.1.tar.gz",
"dir": "libjpeg-turbo-2.1.1",
"build": [
cmd_cmake(
[
@ -154,9 +154,9 @@ deps = {
# "bins": [r"libtiff\*.dll"],
},
"libwebp": {
"url": "http://downloads.webmproject.org/releases/webp/libwebp-1.2.0.tar.gz",
"filename": "libwebp-1.2.0.tar.gz",
"dir": "libwebp-1.2.0",
"url": "http://downloads.webmproject.org/releases/webp/libwebp-1.2.1.tar.gz",
"filename": "libwebp-1.2.1.tar.gz",
"dir": "libwebp-1.2.1",
"build": [
cmd_rmdir(r"output\release-static"), # clean
cmd_nmake(
@ -277,9 +277,9 @@ deps = {
"libs": [r"*.lib"],
},
"harfbuzz": {
"url": "https://github.com/harfbuzz/harfbuzz/archive/2.8.1.zip",
"filename": "harfbuzz-2.8.1.zip",
"dir": "harfbuzz-2.8.1",
"url": "https://github.com/harfbuzz/harfbuzz/archive/2.9.0.zip",
"filename": "harfbuzz-2.9.0.zip",
"dir": "harfbuzz-2.9.0",
"build": [
cmd_cmake("-DHB_HAVE_FREETYPE:BOOL=TRUE"),
cmd_nmake(target="clean"),