mirror of
https://github.com/python-pillow/Pillow.git
synced 2024-11-10 19:56:47 +03:00
Merge branch 'main' into multiline_centered_embedded_color
This commit is contained in:
commit
166654d985
|
@ -37,8 +37,7 @@ python3 -m pip install -U pytest-timeout
|
|||
python3 -m pip install pyroma
|
||||
|
||||
if [[ $(uname) != CYGWIN* ]]; then
|
||||
# TODO Remove condition when NumPy supports 3.11
|
||||
if ! [ "$GHA_PYTHON_VERSION" == "3.11-dev" ]; then python3 -m pip install numpy ; fi
|
||||
python3 -m pip install numpy
|
||||
|
||||
# PyQt6 doesn't support PyPy3
|
||||
if [[ $GHA_PYTHON_VERSION == 3.* ]]; then
|
||||
|
|
3
.github/workflows/cifuzz.yml
vendored
3
.github/workflows/cifuzz.yml
vendored
|
@ -11,6 +11,9 @@ on:
|
|||
- "**.h"
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
Fuzzing:
|
||||
runs-on: ubuntu-latest
|
||||
|
|
3
.github/workflows/macos-install.sh
vendored
3
.github/workflows/macos-install.sh
vendored
|
@ -14,8 +14,7 @@ python3 -m pip install -U pytest-timeout
|
|||
python3 -m pip install pyroma
|
||||
|
||||
echo -e "[openblas]\nlibraries = openblas\nlibrary_dirs = /usr/local/opt/openblas/lib" >> ~/.numpy-site.cfg
|
||||
# TODO Remove condition when NumPy supports 3.11
|
||||
if ! [ "$GHA_PYTHON_VERSION" == "3.11-dev" ]; then python3 -m pip install numpy ; fi
|
||||
python3 -m pip install numpy
|
||||
|
||||
# extra test images
|
||||
pushd depends && ./install_extra_test_images.sh && popd
|
||||
|
|
3
.github/workflows/test-cygwin.yml
vendored
3
.github/workflows/test-cygwin.yml
vendored
|
@ -2,6 +2,9 @@ name: Test Cygwin
|
|||
|
||||
on: [push, pull_request, workflow_dispatch]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: windows-latest
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
repos:
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 22.6.0
|
||||
rev: 22.8.0
|
||||
hooks:
|
||||
- id: black
|
||||
args: ["--target-version", "py37"]
|
||||
|
@ -14,18 +14,18 @@ repos:
|
|||
- id: isort
|
||||
|
||||
- repo: https://github.com/asottile/yesqa
|
||||
rev: v1.3.0
|
||||
rev: v1.4.0
|
||||
hooks:
|
||||
- id: yesqa
|
||||
|
||||
- repo: https://github.com/Lucas-C/pre-commit-hooks
|
||||
rev: v1.3.0
|
||||
rev: v1.3.1
|
||||
hooks:
|
||||
- id: remove-tabs
|
||||
exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.opt$)
|
||||
|
||||
- repo: https://github.com/PyCQA/flake8
|
||||
rev: 5.0.2
|
||||
rev: 5.0.4
|
||||
hooks:
|
||||
- id: flake8
|
||||
additional_dependencies: [flake8-2020, flake8-implicit-str-concat]
|
||||
|
|
30
CHANGES.rst
30
CHANGES.rst
|
@ -5,6 +5,36 @@ Changelog (Pillow)
|
|||
9.3.0 (unreleased)
|
||||
------------------
|
||||
|
||||
- Corrected BMP and TGA palette size when saving #6500
|
||||
[radarhere]
|
||||
|
||||
- Do not call load() before draft() in Image.thumbnail #6539
|
||||
[radarhere]
|
||||
|
||||
- Copy palette when converting from P to PA #6497
|
||||
[radarhere]
|
||||
|
||||
- Allow RGB and RGBA values for PA image putpixel #6504
|
||||
[radarhere]
|
||||
|
||||
- Removed support for tkinter in PyPy before Python 3.6 #6551
|
||||
[nulano]
|
||||
|
||||
- Do not use CCITTFaxDecode filter if libtiff is not available #6518
|
||||
[radarhere]
|
||||
|
||||
- Fallback to not using mmap if buffer is not large enough #6510
|
||||
[radarhere]
|
||||
|
||||
- Fixed writing bytes as ASCII tag #6493
|
||||
[radarhere]
|
||||
|
||||
- Open 1 bit EPS in mode 1 #6499
|
||||
[radarhere]
|
||||
|
||||
- Removed support for tkinter before Python 1.5.2 #6549
|
||||
[radarhere]
|
||||
|
||||
- Allow default ImageDraw font to be set #6484
|
||||
[radarhere, hugovk]
|
||||
|
||||
|
|
BIN
Tests/images/1.eps
Normal file
BIN
Tests/images/1.eps
Normal file
Binary file not shown.
BIN
Tests/images/mmap_error.bmp
Normal file
BIN
Tests/images/mmap_error.bmp
Normal file
Binary file not shown.
After Width: | Height: | Size: 9.0 KiB |
|
@ -39,6 +39,13 @@ def test_invalid_file():
|
|||
BmpImagePlugin.BmpImageFile(fp)
|
||||
|
||||
|
||||
def test_fallback_if_mmap_errors():
|
||||
# This image has been truncated,
|
||||
# so that the buffer is not large enough when using mmap
|
||||
with Image.open("Tests/images/mmap_error.bmp") as im:
|
||||
assert_image_equal_tofile(im, "Tests/images/pal8_offset.bmp")
|
||||
|
||||
|
||||
def test_save_to_bytes():
|
||||
output = io.BytesIO()
|
||||
im = hopper()
|
||||
|
@ -51,6 +58,18 @@ def test_save_to_bytes():
|
|||
assert reloaded.format == "BMP"
|
||||
|
||||
|
||||
def test_small_palette(tmp_path):
|
||||
im = Image.new("P", (1, 1))
|
||||
colors = [0, 0, 0, 125, 125, 125, 255, 255, 255]
|
||||
im.putpalette(colors)
|
||||
|
||||
out = str(tmp_path / "temp.bmp")
|
||||
im.save(out)
|
||||
|
||||
with Image.open(out) as reloaded:
|
||||
assert reloaded.getpalette() == colors
|
||||
|
||||
|
||||
def test_save_too_large(tmp_path):
|
||||
outfile = str(tmp_path / "temp.bmp")
|
||||
with Image.new("RGB", (1, 1)) as im:
|
||||
|
|
|
@ -146,6 +146,11 @@ def test_bytesio_object():
|
|||
assert_image_similar(img, image1_scale1_compare, 5)
|
||||
|
||||
|
||||
def test_1_mode():
|
||||
with Image.open("Tests/images/1.eps") as im:
|
||||
assert im.mode == "1"
|
||||
|
||||
|
||||
def test_image_mode_not_supported(tmp_path):
|
||||
im = hopper("RGBA")
|
||||
tmpfile = str(tmp_path / "temp.eps")
|
||||
|
|
|
@ -6,7 +6,7 @@ import time
|
|||
|
||||
import pytest
|
||||
|
||||
from PIL import Image, PdfParser
|
||||
from PIL import Image, PdfParser, features
|
||||
|
||||
from .helper import hopper, mark_if_feature_version
|
||||
|
||||
|
@ -44,7 +44,7 @@ def test_monochrome(tmp_path):
|
|||
|
||||
# Act / Assert
|
||||
outfile = helper_save_as_pdf(tmp_path, mode)
|
||||
assert os.path.getsize(outfile) < 5000
|
||||
assert os.path.getsize(outfile) < (5000 if features.check("libtiff") else 15000)
|
||||
|
||||
|
||||
def test_greyscale(tmp_path):
|
||||
|
|
|
@ -120,6 +120,18 @@ def test_save(tmp_path):
|
|||
assert test_im.size == (100, 100)
|
||||
|
||||
|
||||
def test_small_palette(tmp_path):
|
||||
im = Image.new("P", (1, 1))
|
||||
colors = [0, 0, 0]
|
||||
im.putpalette(colors)
|
||||
|
||||
out = str(tmp_path / "temp.tga")
|
||||
im.save(out)
|
||||
|
||||
with Image.open(out) as reloaded:
|
||||
assert reloaded.getpalette() == colors
|
||||
|
||||
|
||||
def test_save_wrong_mode(tmp_path):
|
||||
im = hopper("PA")
|
||||
out = str(tmp_path / "temp.tga")
|
||||
|
|
|
@ -185,6 +185,22 @@ def test_iptc(tmp_path):
|
|||
im.save(out)
|
||||
|
||||
|
||||
def test_writing_bytes_to_ascii(tmp_path):
|
||||
im = hopper()
|
||||
info = TiffImagePlugin.ImageFileDirectory_v2()
|
||||
|
||||
tag = TiffTags.TAGS_V2[271]
|
||||
assert tag.type == TiffTags.ASCII
|
||||
|
||||
info[271] = b"test"
|
||||
|
||||
out = str(tmp_path / "temp.tiff")
|
||||
im.save(out, tiffinfo=info)
|
||||
|
||||
with Image.open(out) as reloaded:
|
||||
assert reloaded.tag_v2[271] == "test"
|
||||
|
||||
|
||||
def test_undefined_zero(tmp_path):
|
||||
# Check that the tag has not been changed since this test was created
|
||||
tag = TiffTags.TAGS_V2[45059]
|
||||
|
|
|
@ -215,11 +215,14 @@ class TestImageGetPixel(AccessTest):
|
|||
self.check(mode, 2**15 + 1)
|
||||
self.check(mode, 2**16 - 1)
|
||||
|
||||
@pytest.mark.parametrize("mode", ("P", "PA"))
|
||||
@pytest.mark.parametrize("color", ((255, 0, 0), (255, 0, 0, 255)))
|
||||
def test_p_putpixel_rgb_rgba(self, color):
|
||||
im = Image.new("P", (1, 1), 0)
|
||||
def test_p_putpixel_rgb_rgba(self, mode, color):
|
||||
im = Image.new(mode, (1, 1))
|
||||
im.putpixel((0, 0), color)
|
||||
assert im.convert("RGB").getpixel((0, 0)) == (255, 0, 0)
|
||||
|
||||
alpha = color[3] if len(color) == 4 and mode == "PA" else 255
|
||||
assert im.convert("RGBA").getpixel((0, 0)) == (255, 0, 0, alpha)
|
||||
|
||||
|
||||
@pytest.mark.skipif(cffi is None, reason="No CFFI")
|
||||
|
@ -340,12 +343,15 @@ class TestCffi(AccessTest):
|
|||
# pixels can contain garbage if image is released
|
||||
assert px[i, 0] == 0
|
||||
|
||||
def test_p_putpixel_rgb_rgba(self):
|
||||
for color in [(255, 0, 0), (255, 0, 0, 255)]:
|
||||
im = Image.new("P", (1, 1), 0)
|
||||
@pytest.mark.parametrize("mode", ("P", "PA"))
|
||||
def test_p_putpixel_rgb_rgba(self, mode):
|
||||
for color in [(255, 0, 0), (255, 0, 0, 127)]:
|
||||
im = Image.new(mode, (1, 1))
|
||||
access = PyAccess.new(im, False)
|
||||
access.putpixel((0, 0), color)
|
||||
assert im.convert("RGB").getpixel((0, 0)) == (255, 0, 0)
|
||||
|
||||
alpha = color[3] if len(color) == 4 and mode == "PA" else 255
|
||||
assert im.convert("RGBA").getpixel((0, 0)) == (255, 0, 0, alpha)
|
||||
|
||||
|
||||
class TestImagePutPixelError(AccessTest):
|
||||
|
|
|
@ -236,6 +236,12 @@ def test_p2pa_alpha():
|
|||
assert im_a.getpixel((x, y)) == alpha
|
||||
|
||||
|
||||
def test_p2pa_palette():
|
||||
with Image.open("Tests/images/tiny.png") as im:
|
||||
im_pa = im.convert("PA")
|
||||
assert im_pa.getpalette() == im.getpalette()
|
||||
|
||||
|
||||
def test_matrix_illegal_conversion():
|
||||
# Arrange
|
||||
im = hopper("CMYK")
|
||||
|
|
|
@ -5,90 +5,109 @@ from PIL import Image, ImageFilter
|
|||
from .helper import assert_image_equal, hopper
|
||||
|
||||
|
||||
def test_sanity():
|
||||
def apply_filter(filter_to_apply):
|
||||
for mode in ["L", "RGB", "CMYK"]:
|
||||
im = hopper(mode)
|
||||
out = im.filter(filter_to_apply)
|
||||
assert out.mode == im.mode
|
||||
assert out.size == im.size
|
||||
@pytest.mark.parametrize(
|
||||
"filter_to_apply",
|
||||
(
|
||||
ImageFilter.BLUR,
|
||||
ImageFilter.CONTOUR,
|
||||
ImageFilter.DETAIL,
|
||||
ImageFilter.EDGE_ENHANCE,
|
||||
ImageFilter.EDGE_ENHANCE_MORE,
|
||||
ImageFilter.EMBOSS,
|
||||
ImageFilter.FIND_EDGES,
|
||||
ImageFilter.SMOOTH,
|
||||
ImageFilter.SMOOTH_MORE,
|
||||
ImageFilter.SHARPEN,
|
||||
ImageFilter.MaxFilter,
|
||||
ImageFilter.MedianFilter,
|
||||
ImageFilter.MinFilter,
|
||||
ImageFilter.ModeFilter,
|
||||
ImageFilter.GaussianBlur,
|
||||
ImageFilter.GaussianBlur(5),
|
||||
ImageFilter.BoxBlur(5),
|
||||
ImageFilter.UnsharpMask,
|
||||
ImageFilter.UnsharpMask(10),
|
||||
),
|
||||
)
|
||||
@pytest.mark.parametrize("mode", ("L", "RGB", "CMYK"))
|
||||
def test_sanity(filter_to_apply, mode):
|
||||
im = hopper(mode)
|
||||
out = im.filter(filter_to_apply)
|
||||
assert out.mode == im.mode
|
||||
assert out.size == im.size
|
||||
|
||||
apply_filter(ImageFilter.BLUR)
|
||||
apply_filter(ImageFilter.CONTOUR)
|
||||
apply_filter(ImageFilter.DETAIL)
|
||||
apply_filter(ImageFilter.EDGE_ENHANCE)
|
||||
apply_filter(ImageFilter.EDGE_ENHANCE_MORE)
|
||||
apply_filter(ImageFilter.EMBOSS)
|
||||
apply_filter(ImageFilter.FIND_EDGES)
|
||||
apply_filter(ImageFilter.SMOOTH)
|
||||
apply_filter(ImageFilter.SMOOTH_MORE)
|
||||
apply_filter(ImageFilter.SHARPEN)
|
||||
apply_filter(ImageFilter.MaxFilter)
|
||||
apply_filter(ImageFilter.MedianFilter)
|
||||
apply_filter(ImageFilter.MinFilter)
|
||||
apply_filter(ImageFilter.ModeFilter)
|
||||
apply_filter(ImageFilter.GaussianBlur)
|
||||
apply_filter(ImageFilter.GaussianBlur(5))
|
||||
apply_filter(ImageFilter.BoxBlur(5))
|
||||
apply_filter(ImageFilter.UnsharpMask)
|
||||
apply_filter(ImageFilter.UnsharpMask(10))
|
||||
|
||||
@pytest.mark.parametrize("mode", ("L", "RGB", "CMYK"))
|
||||
def test_sanity_error(mode):
|
||||
with pytest.raises(TypeError):
|
||||
apply_filter("hello")
|
||||
im = hopper(mode)
|
||||
im.filter("hello")
|
||||
|
||||
|
||||
def test_crash():
|
||||
|
||||
# crashes on small images
|
||||
im = Image.new("RGB", (1, 1))
|
||||
im.filter(ImageFilter.SMOOTH)
|
||||
|
||||
im = Image.new("RGB", (2, 2))
|
||||
im.filter(ImageFilter.SMOOTH)
|
||||
|
||||
im = Image.new("RGB", (3, 3))
|
||||
# crashes on small images
|
||||
@pytest.mark.parametrize("size", ((1, 1), (2, 2), (3, 3)))
|
||||
def test_crash(size):
|
||||
im = Image.new("RGB", size)
|
||||
im.filter(ImageFilter.SMOOTH)
|
||||
|
||||
|
||||
def test_modefilter():
|
||||
def modefilter(mode):
|
||||
im = Image.new(mode, (3, 3), None)
|
||||
im.putdata(list(range(9)))
|
||||
# image is:
|
||||
# 0 1 2
|
||||
# 3 4 5
|
||||
# 6 7 8
|
||||
mod = im.filter(ImageFilter.ModeFilter).getpixel((1, 1))
|
||||
im.putdata([0, 0, 1, 2, 5, 1, 5, 2, 0]) # mode=0
|
||||
mod2 = im.filter(ImageFilter.ModeFilter).getpixel((1, 1))
|
||||
return mod, mod2
|
||||
|
||||
assert modefilter("1") == (4, 0)
|
||||
assert modefilter("L") == (4, 0)
|
||||
assert modefilter("P") == (4, 0)
|
||||
assert modefilter("RGB") == ((4, 0, 0), (0, 0, 0))
|
||||
@pytest.mark.parametrize(
|
||||
"mode, expected",
|
||||
(
|
||||
("1", (4, 0)),
|
||||
("L", (4, 0)),
|
||||
("P", (4, 0)),
|
||||
("RGB", ((4, 0, 0), (0, 0, 0))),
|
||||
),
|
||||
)
|
||||
def test_modefilter(mode, expected):
|
||||
im = Image.new(mode, (3, 3), None)
|
||||
im.putdata(list(range(9)))
|
||||
# image is:
|
||||
# 0 1 2
|
||||
# 3 4 5
|
||||
# 6 7 8
|
||||
mod = im.filter(ImageFilter.ModeFilter).getpixel((1, 1))
|
||||
im.putdata([0, 0, 1, 2, 5, 1, 5, 2, 0]) # mode=0
|
||||
mod2 = im.filter(ImageFilter.ModeFilter).getpixel((1, 1))
|
||||
assert (mod, mod2) == expected
|
||||
|
||||
|
||||
def test_rankfilter():
|
||||
def rankfilter(mode):
|
||||
im = Image.new(mode, (3, 3), None)
|
||||
im.putdata(list(range(9)))
|
||||
# image is:
|
||||
# 0 1 2
|
||||
# 3 4 5
|
||||
# 6 7 8
|
||||
minimum = im.filter(ImageFilter.MinFilter).getpixel((1, 1))
|
||||
med = im.filter(ImageFilter.MedianFilter).getpixel((1, 1))
|
||||
maximum = im.filter(ImageFilter.MaxFilter).getpixel((1, 1))
|
||||
return minimum, med, maximum
|
||||
@pytest.mark.parametrize(
|
||||
"mode, expected",
|
||||
(
|
||||
("1", (0, 4, 8)),
|
||||
("L", (0, 4, 8)),
|
||||
("RGB", ((0, 0, 0), (4, 0, 0), (8, 0, 0))),
|
||||
("I", (0, 4, 8)),
|
||||
("F", (0.0, 4.0, 8.0)),
|
||||
),
|
||||
)
|
||||
def test_rankfilter(mode, expected):
|
||||
im = Image.new(mode, (3, 3), None)
|
||||
im.putdata(list(range(9)))
|
||||
# image is:
|
||||
# 0 1 2
|
||||
# 3 4 5
|
||||
# 6 7 8
|
||||
minimum = im.filter(ImageFilter.MinFilter).getpixel((1, 1))
|
||||
med = im.filter(ImageFilter.MedianFilter).getpixel((1, 1))
|
||||
maximum = im.filter(ImageFilter.MaxFilter).getpixel((1, 1))
|
||||
assert (minimum, med, maximum) == expected
|
||||
|
||||
assert rankfilter("1") == (0, 4, 8)
|
||||
assert rankfilter("L") == (0, 4, 8)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"filter", (ImageFilter.MinFilter, ImageFilter.MedianFilter, ImageFilter.MaxFilter)
|
||||
)
|
||||
def test_rankfilter_error(filter):
|
||||
with pytest.raises(ValueError):
|
||||
rankfilter("P")
|
||||
assert rankfilter("RGB") == ((0, 0, 0), (4, 0, 0), (8, 0, 0))
|
||||
assert rankfilter("I") == (0, 4, 8)
|
||||
assert rankfilter("F") == (0.0, 4.0, 8.0)
|
||||
im = Image.new("P", (3, 3), None)
|
||||
im.putdata(list(range(9)))
|
||||
# image is:
|
||||
# 0 1 2
|
||||
# 3 4 5
|
||||
# 6 7 8
|
||||
im.filter(filter).getpixel((1, 1))
|
||||
|
||||
|
||||
def test_rankfilter_properties():
|
||||
|
@ -110,7 +129,8 @@ def test_kernel_not_enough_coefficients():
|
|||
ImageFilter.Kernel((3, 3), (0, 0))
|
||||
|
||||
|
||||
def test_consistency_3x3():
|
||||
@pytest.mark.parametrize("mode", ("L", "LA", "RGB", "CMYK"))
|
||||
def test_consistency_3x3(mode):
|
||||
with Image.open("Tests/images/hopper.bmp") as source:
|
||||
with Image.open("Tests/images/hopper_emboss.bmp") as reference:
|
||||
kernel = ImageFilter.Kernel(
|
||||
|
@ -125,14 +145,14 @@ def test_consistency_3x3():
|
|||
source = source.split() * 2
|
||||
reference = reference.split() * 2
|
||||
|
||||
for mode in ["L", "LA", "RGB", "CMYK"]:
|
||||
assert_image_equal(
|
||||
Image.merge(mode, source[: len(mode)]).filter(kernel),
|
||||
Image.merge(mode, reference[: len(mode)]),
|
||||
)
|
||||
assert_image_equal(
|
||||
Image.merge(mode, source[: len(mode)]).filter(kernel),
|
||||
Image.merge(mode, reference[: len(mode)]),
|
||||
)
|
||||
|
||||
|
||||
def test_consistency_5x5():
|
||||
@pytest.mark.parametrize("mode", ("L", "LA", "RGB", "CMYK"))
|
||||
def test_consistency_5x5(mode):
|
||||
with Image.open("Tests/images/hopper.bmp") as source:
|
||||
with Image.open("Tests/images/hopper_emboss_more.bmp") as reference:
|
||||
kernel = ImageFilter.Kernel(
|
||||
|
@ -149,8 +169,7 @@ def test_consistency_5x5():
|
|||
source = source.split() * 2
|
||||
reference = reference.split() * 2
|
||||
|
||||
for mode in ["L", "LA", "RGB", "CMYK"]:
|
||||
assert_image_equal(
|
||||
Image.merge(mode, source[: len(mode)]).filter(kernel),
|
||||
Image.merge(mode, reference[: len(mode)]),
|
||||
)
|
||||
assert_image_equal(
|
||||
Image.merge(mode, source[: len(mode)]).filter(kernel),
|
||||
Image.merge(mode, reference[: len(mode)]),
|
||||
)
|
||||
|
|
|
@ -38,58 +38,64 @@ gradients_image = Image.open("Tests/images/radial_gradients.png")
|
|||
gradients_image.load()
|
||||
|
||||
|
||||
def test_args_factor():
|
||||
@pytest.mark.parametrize(
|
||||
"size, expected",
|
||||
(
|
||||
(3, (4, 4)),
|
||||
((3, 1), (4, 10)),
|
||||
((1, 3), (10, 4)),
|
||||
),
|
||||
)
|
||||
def test_args_factor(size, expected):
|
||||
im = Image.new("L", (10, 10))
|
||||
|
||||
assert (4, 4) == im.reduce(3).size
|
||||
assert (4, 10) == im.reduce((3, 1)).size
|
||||
assert (10, 4) == im.reduce((1, 3)).size
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
im.reduce(0)
|
||||
with pytest.raises(TypeError):
|
||||
im.reduce(2.0)
|
||||
with pytest.raises(ValueError):
|
||||
im.reduce((0, 10))
|
||||
assert expected == im.reduce(size).size
|
||||
|
||||
|
||||
def test_args_box():
|
||||
@pytest.mark.parametrize(
|
||||
"size, expected_error", ((0, ValueError), (2.0, TypeError), ((0, 10), ValueError))
|
||||
)
|
||||
def test_args_factor_error(size, expected_error):
|
||||
im = Image.new("L", (10, 10))
|
||||
|
||||
assert (5, 5) == im.reduce(2, (0, 0, 10, 10)).size
|
||||
assert (1, 1) == im.reduce(2, (5, 5, 6, 6)).size
|
||||
|
||||
with pytest.raises(TypeError):
|
||||
im.reduce(2, "stri")
|
||||
with pytest.raises(TypeError):
|
||||
im.reduce(2, 2)
|
||||
with pytest.raises(ValueError):
|
||||
im.reduce(2, (0, 0, 11, 10))
|
||||
with pytest.raises(ValueError):
|
||||
im.reduce(2, (0, 0, 10, 11))
|
||||
with pytest.raises(ValueError):
|
||||
im.reduce(2, (-1, 0, 10, 10))
|
||||
with pytest.raises(ValueError):
|
||||
im.reduce(2, (0, -1, 10, 10))
|
||||
with pytest.raises(ValueError):
|
||||
im.reduce(2, (0, 5, 10, 5))
|
||||
with pytest.raises(ValueError):
|
||||
im.reduce(2, (5, 0, 5, 10))
|
||||
with pytest.raises(expected_error):
|
||||
im.reduce(size)
|
||||
|
||||
|
||||
def test_unsupported_modes():
|
||||
@pytest.mark.parametrize(
|
||||
"size, expected",
|
||||
(
|
||||
((0, 0, 10, 10), (5, 5)),
|
||||
((5, 5, 6, 6), (1, 1)),
|
||||
),
|
||||
)
|
||||
def test_args_box(size, expected):
|
||||
im = Image.new("L", (10, 10))
|
||||
assert expected == im.reduce(2, size).size
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"size, expected_error",
|
||||
(
|
||||
("stri", TypeError),
|
||||
((0, 0, 11, 10), ValueError),
|
||||
((0, 0, 10, 11), ValueError),
|
||||
((-1, 0, 10, 10), ValueError),
|
||||
((0, -1, 10, 10), ValueError),
|
||||
((0, 5, 10, 5), ValueError),
|
||||
((5, 0, 5, 10), ValueError),
|
||||
),
|
||||
)
|
||||
def test_args_box_error(size, expected_error):
|
||||
im = Image.new("L", (10, 10))
|
||||
with pytest.raises(expected_error):
|
||||
im.reduce(2, size).size
|
||||
|
||||
|
||||
@pytest.mark.parametrize("mode", ("P", "1", "I;16"))
|
||||
def test_unsupported_modes(mode):
|
||||
im = Image.new("P", (10, 10))
|
||||
with pytest.raises(ValueError):
|
||||
im.reduce(3)
|
||||
|
||||
im = Image.new("1", (10, 10))
|
||||
with pytest.raises(ValueError):
|
||||
im.reduce(3)
|
||||
|
||||
im = Image.new("I;16", (10, 10))
|
||||
with pytest.raises(ValueError):
|
||||
im.reduce(3)
|
||||
|
||||
|
||||
def get_image(mode):
|
||||
mode_info = ImageMode.getmode(mode)
|
||||
|
@ -197,63 +203,69 @@ def test_mode_L():
|
|||
compare_reduce_with_box(im, factor)
|
||||
|
||||
|
||||
def test_mode_LA():
|
||||
@pytest.mark.parametrize("factor", remarkable_factors)
|
||||
def test_mode_LA(factor):
|
||||
im = get_image("LA")
|
||||
for factor in remarkable_factors:
|
||||
compare_reduce_with_reference(im, factor, 0.8, 5)
|
||||
compare_reduce_with_reference(im, factor, 0.8, 5)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("factor", remarkable_factors)
|
||||
def test_mode_LA_opaque(factor):
|
||||
im = get_image("LA")
|
||||
# With opaque alpha, an error should be way smaller.
|
||||
im.putalpha(Image.new("L", im.size, 255))
|
||||
for factor in remarkable_factors:
|
||||
compare_reduce_with_reference(im, factor)
|
||||
compare_reduce_with_box(im, factor)
|
||||
compare_reduce_with_reference(im, factor)
|
||||
compare_reduce_with_box(im, factor)
|
||||
|
||||
|
||||
def test_mode_La():
|
||||
@pytest.mark.parametrize("factor", remarkable_factors)
|
||||
def test_mode_La(factor):
|
||||
im = get_image("La")
|
||||
for factor in remarkable_factors:
|
||||
compare_reduce_with_reference(im, factor)
|
||||
compare_reduce_with_box(im, factor)
|
||||
compare_reduce_with_reference(im, factor)
|
||||
compare_reduce_with_box(im, factor)
|
||||
|
||||
|
||||
def test_mode_RGB():
|
||||
@pytest.mark.parametrize("factor", remarkable_factors)
|
||||
def test_mode_RGB(factor):
|
||||
im = get_image("RGB")
|
||||
for factor in remarkable_factors:
|
||||
compare_reduce_with_reference(im, factor)
|
||||
compare_reduce_with_box(im, factor)
|
||||
compare_reduce_with_reference(im, factor)
|
||||
compare_reduce_with_box(im, factor)
|
||||
|
||||
|
||||
def test_mode_RGBA():
|
||||
@pytest.mark.parametrize("factor", remarkable_factors)
|
||||
def test_mode_RGBA(factor):
|
||||
im = get_image("RGBA")
|
||||
for factor in remarkable_factors:
|
||||
compare_reduce_with_reference(im, factor, 0.8, 5)
|
||||
compare_reduce_with_reference(im, factor, 0.8, 5)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("factor", remarkable_factors)
|
||||
def test_mode_RGBA_opaque(factor):
|
||||
im = get_image("RGBA")
|
||||
# With opaque alpha, an error should be way smaller.
|
||||
im.putalpha(Image.new("L", im.size, 255))
|
||||
for factor in remarkable_factors:
|
||||
compare_reduce_with_reference(im, factor)
|
||||
compare_reduce_with_box(im, factor)
|
||||
compare_reduce_with_reference(im, factor)
|
||||
compare_reduce_with_box(im, factor)
|
||||
|
||||
|
||||
def test_mode_RGBa():
|
||||
@pytest.mark.parametrize("factor", remarkable_factors)
|
||||
def test_mode_RGBa(factor):
|
||||
im = get_image("RGBa")
|
||||
for factor in remarkable_factors:
|
||||
compare_reduce_with_reference(im, factor)
|
||||
compare_reduce_with_box(im, factor)
|
||||
compare_reduce_with_reference(im, factor)
|
||||
compare_reduce_with_box(im, factor)
|
||||
|
||||
|
||||
def test_mode_I():
|
||||
@pytest.mark.parametrize("factor", remarkable_factors)
|
||||
def test_mode_I(factor):
|
||||
im = get_image("I")
|
||||
for factor in remarkable_factors:
|
||||
compare_reduce_with_reference(im, factor)
|
||||
compare_reduce_with_box(im, factor)
|
||||
compare_reduce_with_reference(im, factor)
|
||||
compare_reduce_with_box(im, factor)
|
||||
|
||||
|
||||
def test_mode_F():
|
||||
@pytest.mark.parametrize("factor", remarkable_factors)
|
||||
def test_mode_F(factor):
|
||||
im = get_image("F")
|
||||
for factor in remarkable_factors:
|
||||
compare_reduce_with_reference(im, factor, 0, 0)
|
||||
compare_reduce_with_box(im, factor)
|
||||
compare_reduce_with_reference(im, factor, 0, 0)
|
||||
compare_reduce_with_box(im, factor)
|
||||
|
||||
|
||||
@skip_unless_feature("jpg_2000")
|
||||
|
|
|
@ -97,6 +97,28 @@ def test_load_first():
|
|||
im.thumbnail((64, 64))
|
||||
assert im.size == (64, 10)
|
||||
|
||||
# Test thumbnail(), without draft(),
|
||||
# on an image that is large enough once load() has changed the size
|
||||
with Image.open("Tests/images/g4_orientation_5.tif") as im:
|
||||
im.thumbnail((590, 88), reducing_gap=None)
|
||||
assert im.size == (590, 88)
|
||||
|
||||
|
||||
def test_load_first_unless_jpeg():
|
||||
# Test that thumbnail() still uses draft() for JPEG
|
||||
with Image.open("Tests/images/hopper.jpg") as im:
|
||||
draft = im.draft
|
||||
|
||||
def im_draft(mode, size):
|
||||
result = draft(mode, size)
|
||||
assert result is not None
|
||||
|
||||
return result
|
||||
|
||||
im.draft = im_draft
|
||||
|
||||
im.thumbnail((64, 64))
|
||||
|
||||
|
||||
# valgrind test is failing with memory allocated in libjpeg
|
||||
@pytest.mark.valgrind_known_error(reason="Known Failing")
|
||||
|
|
|
@ -75,23 +75,25 @@ class TestImageTransform:
|
|||
|
||||
assert_image_equal(transformed, scaled)
|
||||
|
||||
def test_fill(self):
|
||||
for mode, pixel in [
|
||||
["RGB", (255, 0, 0)],
|
||||
["RGBA", (255, 0, 0, 255)],
|
||||
["LA", (76, 0)],
|
||||
]:
|
||||
im = hopper(mode)
|
||||
(w, h) = im.size
|
||||
transformed = im.transform(
|
||||
im.size,
|
||||
Image.Transform.EXTENT,
|
||||
(0, 0, w * 2, h * 2),
|
||||
Image.Resampling.BILINEAR,
|
||||
fillcolor="red",
|
||||
)
|
||||
|
||||
assert transformed.getpixel((w - 1, h - 1)) == pixel
|
||||
@pytest.mark.parametrize(
|
||||
"mode, expected_pixel",
|
||||
(
|
||||
("RGB", (255, 0, 0)),
|
||||
("RGBA", (255, 0, 0, 255)),
|
||||
("LA", (76, 0)),
|
||||
),
|
||||
)
|
||||
def test_fill(self, mode, expected_pixel):
|
||||
im = hopper(mode)
|
||||
(w, h) = im.size
|
||||
transformed = im.transform(
|
||||
im.size,
|
||||
Image.Transform.EXTENT,
|
||||
(0, 0, w * 2, h * 2),
|
||||
Image.Resampling.BILINEAR,
|
||||
fillcolor="red",
|
||||
)
|
||||
assert transformed.getpixel((w - 1, h - 1)) == expected_pixel
|
||||
|
||||
def test_mesh(self):
|
||||
# this should be a checkerboard of halfsized hoppers in ul, lr
|
||||
|
@ -222,14 +224,12 @@ class TestImageTransform:
|
|||
with pytest.raises(ValueError):
|
||||
im.transform((100, 100), None)
|
||||
|
||||
def test_unknown_resampling_filter(self):
|
||||
@pytest.mark.parametrize("resample", (Image.Resampling.BOX, "unknown"))
|
||||
def test_unknown_resampling_filter(self, resample):
|
||||
with hopper() as im:
|
||||
(w, h) = im.size
|
||||
for resample in (Image.Resampling.BOX, "unknown"):
|
||||
with pytest.raises(ValueError):
|
||||
im.transform(
|
||||
(100, 100), Image.Transform.EXTENT, (0, 0, w, h), resample
|
||||
)
|
||||
with pytest.raises(ValueError):
|
||||
im.transform((100, 100), Image.Transform.EXTENT, (0, 0, w, h), resample)
|
||||
|
||||
|
||||
class TestImageTransformAffine:
|
||||
|
@ -239,7 +239,16 @@ class TestImageTransformAffine:
|
|||
im = hopper("RGB")
|
||||
return im.crop((10, 20, im.width - 10, im.height - 20))
|
||||
|
||||
def _test_rotate(self, deg, transpose):
|
||||
@pytest.mark.parametrize(
|
||||
"deg, transpose",
|
||||
(
|
||||
(0, None),
|
||||
(90, Image.Transpose.ROTATE_90),
|
||||
(180, Image.Transpose.ROTATE_180),
|
||||
(270, Image.Transpose.ROTATE_270),
|
||||
),
|
||||
)
|
||||
def test_rotate(self, deg, transpose):
|
||||
im = self._test_image()
|
||||
|
||||
angle = -math.radians(deg)
|
||||
|
@ -271,77 +280,65 @@ class TestImageTransformAffine:
|
|||
)
|
||||
assert_image_equal(transposed, transformed)
|
||||
|
||||
def test_rotate_0_deg(self):
|
||||
self._test_rotate(0, None)
|
||||
|
||||
def test_rotate_90_deg(self):
|
||||
self._test_rotate(90, Image.Transpose.ROTATE_90)
|
||||
|
||||
def test_rotate_180_deg(self):
|
||||
self._test_rotate(180, Image.Transpose.ROTATE_180)
|
||||
|
||||
def test_rotate_270_deg(self):
|
||||
self._test_rotate(270, Image.Transpose.ROTATE_270)
|
||||
|
||||
def _test_resize(self, scale, epsilonscale):
|
||||
@pytest.mark.parametrize(
|
||||
"scale, epsilon_scale",
|
||||
(
|
||||
(1.1, 6.9),
|
||||
(1.5, 5.5),
|
||||
(2.0, 5.5),
|
||||
(2.3, 3.7),
|
||||
(2.5, 3.7),
|
||||
),
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
"resample,epsilon",
|
||||
(
|
||||
(Image.Resampling.NEAREST, 0),
|
||||
(Image.Resampling.BILINEAR, 2),
|
||||
(Image.Resampling.BICUBIC, 1),
|
||||
),
|
||||
)
|
||||
def test_resize(self, scale, epsilon_scale, resample, epsilon):
|
||||
im = self._test_image()
|
||||
|
||||
size_up = int(round(im.width * scale)), int(round(im.height * scale))
|
||||
matrix_up = [1 / scale, 0, 0, 0, 1 / scale, 0, 0, 0]
|
||||
matrix_down = [scale, 0, 0, 0, scale, 0, 0, 0]
|
||||
|
||||
for resample, epsilon in [
|
||||
transformed = im.transform(size_up, self.transform, matrix_up, resample)
|
||||
transformed = transformed.transform(
|
||||
im.size, self.transform, matrix_down, resample
|
||||
)
|
||||
assert_image_similar(transformed, im, epsilon * epsilon_scale)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"x, y, epsilon_scale",
|
||||
(
|
||||
(0.1, 0, 3.7),
|
||||
(0.6, 0, 9.1),
|
||||
(50, 50, 0),
|
||||
),
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
"resample, epsilon",
|
||||
(
|
||||
(Image.Resampling.NEAREST, 0),
|
||||
(Image.Resampling.BILINEAR, 2),
|
||||
(Image.Resampling.BILINEAR, 1.5),
|
||||
(Image.Resampling.BICUBIC, 1),
|
||||
]:
|
||||
transformed = im.transform(size_up, self.transform, matrix_up, resample)
|
||||
transformed = transformed.transform(
|
||||
im.size, self.transform, matrix_down, resample
|
||||
)
|
||||
assert_image_similar(transformed, im, epsilon * epsilonscale)
|
||||
|
||||
def test_resize_1_1x(self):
|
||||
self._test_resize(1.1, 6.9)
|
||||
|
||||
def test_resize_1_5x(self):
|
||||
self._test_resize(1.5, 5.5)
|
||||
|
||||
def test_resize_2_0x(self):
|
||||
self._test_resize(2.0, 5.5)
|
||||
|
||||
def test_resize_2_3x(self):
|
||||
self._test_resize(2.3, 3.7)
|
||||
|
||||
def test_resize_2_5x(self):
|
||||
self._test_resize(2.5, 3.7)
|
||||
|
||||
def _test_translate(self, x, y, epsilonscale):
|
||||
),
|
||||
)
|
||||
def test_translate(self, x, y, epsilon_scale, resample, epsilon):
|
||||
im = self._test_image()
|
||||
|
||||
size_up = int(round(im.width + x)), int(round(im.height + y))
|
||||
matrix_up = [1, 0, -x, 0, 1, -y, 0, 0]
|
||||
matrix_down = [1, 0, x, 0, 1, y, 0, 0]
|
||||
|
||||
for resample, epsilon in [
|
||||
(Image.Resampling.NEAREST, 0),
|
||||
(Image.Resampling.BILINEAR, 1.5),
|
||||
(Image.Resampling.BICUBIC, 1),
|
||||
]:
|
||||
transformed = im.transform(size_up, self.transform, matrix_up, resample)
|
||||
transformed = transformed.transform(
|
||||
im.size, self.transform, matrix_down, resample
|
||||
)
|
||||
assert_image_similar(transformed, im, epsilon * epsilonscale)
|
||||
|
||||
def test_translate_0_1(self):
|
||||
self._test_translate(0.1, 0, 3.7)
|
||||
|
||||
def test_translate_0_6(self):
|
||||
self._test_translate(0.6, 0, 9.1)
|
||||
|
||||
def test_translate_50(self):
|
||||
self._test_translate(50, 50, 0)
|
||||
transformed = im.transform(size_up, self.transform, matrix_up, resample)
|
||||
transformed = transformed.transform(
|
||||
im.size, self.transform, matrix_down, resample
|
||||
)
|
||||
assert_image_similar(transformed, im, epsilon * epsilon_scale)
|
||||
|
||||
|
||||
class TestImageTransformPerspective(TestImageTransformAffine):
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -837,6 +837,24 @@ Pillow reads and writes TGA images containing ``L``, ``LA``, ``P``,
|
|||
``RGB``, and ``RGBA`` data. Pillow can read and write both uncompressed and
|
||||
run-length encoded TGAs.
|
||||
|
||||
The :py:meth:`~PIL.Image.Image.save` method can take the following keyword arguments:
|
||||
|
||||
**compression**
|
||||
If set to "tga_rle", the file will be run-length encoded.
|
||||
|
||||
.. versionadded:: 5.3.0
|
||||
|
||||
**id_section**
|
||||
The identification field.
|
||||
|
||||
.. versionadded:: 5.3.0
|
||||
|
||||
**orientation**
|
||||
If present and a positive number, the first pixel is for the top left corner,
|
||||
rather than the bottom left corner.
|
||||
|
||||
.. versionadded:: 5.3.0
|
||||
|
||||
TIFF
|
||||
^^^^
|
||||
|
||||
|
|
|
@ -53,9 +53,9 @@ Functions
|
|||
To protect against potential DOS attacks caused by "`decompression bombs`_" (i.e. malicious files
|
||||
which decompress into a huge amount of data and are designed to crash or cause disruption by using up
|
||||
a lot of memory), Pillow will issue a ``DecompressionBombWarning`` if the number of pixels in an
|
||||
image is over a certain limit, :py:data:`PIL.Image.MAX_IMAGE_PIXELS`.
|
||||
image is over a certain limit, :py:data:`MAX_IMAGE_PIXELS`.
|
||||
|
||||
This threshold can be changed by setting :py:data:`PIL.Image.MAX_IMAGE_PIXELS`. It can be disabled
|
||||
This threshold can be changed by setting :py:data:`MAX_IMAGE_PIXELS`. It can be disabled
|
||||
by setting ``Image.MAX_IMAGE_PIXELS = None``.
|
||||
|
||||
If desired, the warning can be turned into an error with
|
||||
|
@ -63,7 +63,7 @@ Functions
|
|||
``warnings.simplefilter('ignore', Image.DecompressionBombWarning)``. See also
|
||||
`the logging documentation`_ to have warnings output to the logging facility instead of stderr.
|
||||
|
||||
If the number of pixels is greater than twice :py:data:`PIL.Image.MAX_IMAGE_PIXELS`, then a
|
||||
If the number of pixels is greater than twice :py:data:`MAX_IMAGE_PIXELS`, then a
|
||||
``DecompressionBombError`` will be raised instead.
|
||||
|
||||
.. _decompression bombs: https://en.wikipedia.org/wiki/Zip_bomb
|
||||
|
@ -255,7 +255,7 @@ This rotates the input image by ``theta`` degrees counter clockwise:
|
|||
.. automethod:: PIL.Image.Image.transform
|
||||
.. automethod:: PIL.Image.Image.transpose
|
||||
|
||||
This flips the input image by using the :data:`PIL.Image.Transpose.FLIP_LEFT_RIGHT`
|
||||
This flips the input image by using the :data:`Transpose.FLIP_LEFT_RIGHT`
|
||||
method.
|
||||
|
||||
.. code-block:: python
|
||||
|
|
|
@ -73,7 +73,7 @@ Access using negative indexes is also possible.
|
|||
Modifies the pixel at x,y. The color is given as a single
|
||||
numerical value for single band images, and a tuple for
|
||||
multi-band images. In addition to this, RGB and RGBA tuples
|
||||
are accepted for P images.
|
||||
are accepted for P and PA images.
|
||||
|
||||
:param xy: The pixel coordinate, given as (x, y).
|
||||
:param color: The pixel value according to its mode. e.g. tuple (r, g, b) for RGB mode)
|
||||
|
|
|
@ -34,7 +34,11 @@ project_urls =
|
|||
Twitter=https://twitter.com/PythonPillow
|
||||
|
||||
[options]
|
||||
packages = PIL
|
||||
python_requires = >=3.7
|
||||
include_package_data = True
|
||||
package_dir =
|
||||
= src
|
||||
|
||||
[options.extras_require]
|
||||
docs =
|
||||
|
|
3
setup.py
3
setup.py
|
@ -999,9 +999,6 @@ try:
|
|||
version=PILLOW_VERSION,
|
||||
cmdclass={"build_ext": pil_build_ext},
|
||||
ext_modules=ext_modules,
|
||||
include_package_data=True,
|
||||
packages=["PIL"],
|
||||
package_dir={"": "src"},
|
||||
zip_safe=not (debug_build() or PLATFORM_MINGW),
|
||||
)
|
||||
except RequiredDependencyException as err:
|
||||
|
|
|
@ -375,6 +375,16 @@ def _save(im, fp, filename, bitmap_header=True):
|
|||
header = 40 # or 64 for OS/2 version 2
|
||||
image = stride * im.size[1]
|
||||
|
||||
if im.mode == "1":
|
||||
palette = b"".join(o8(i) * 4 for i in (0, 255))
|
||||
elif im.mode == "L":
|
||||
palette = b"".join(o8(i) * 4 for i in range(256))
|
||||
elif im.mode == "P":
|
||||
palette = im.im.getpalette("RGB", "BGRX")
|
||||
colors = len(palette) // 4
|
||||
else:
|
||||
palette = None
|
||||
|
||||
# bitmap header
|
||||
if bitmap_header:
|
||||
offset = 14 + header + colors * 4
|
||||
|
@ -405,14 +415,8 @@ def _save(im, fp, filename, bitmap_header=True):
|
|||
|
||||
fp.write(b"\0" * (header - 40)) # padding (for OS/2 format)
|
||||
|
||||
if im.mode == "1":
|
||||
for i in (0, 255):
|
||||
fp.write(o8(i) * 4)
|
||||
elif im.mode == "L":
|
||||
for i in range(256):
|
||||
fp.write(o8(i) * 4)
|
||||
elif im.mode == "P":
|
||||
fp.write(im.im.getpalette("RGB", "BGRX"))
|
||||
if palette:
|
||||
fp.write(palette)
|
||||
|
||||
ImageFile._save(im, fp, [("raw", (0, 0) + im.size, 0, (rawmode, stride, -1))])
|
||||
|
||||
|
|
|
@ -288,11 +288,14 @@ class EpsImageFile(ImageFile.ImageFile):
|
|||
# Encoded bitmapped image.
|
||||
x, y, bi, mo = s[11:].split(None, 7)[:4]
|
||||
|
||||
if int(bi) != 8:
|
||||
break
|
||||
try:
|
||||
self.mode = self.mode_map[int(mo)]
|
||||
except ValueError:
|
||||
if int(bi) == 1:
|
||||
self.mode = "1"
|
||||
elif int(bi) == 8:
|
||||
try:
|
||||
self.mode = self.mode_map[int(mo)]
|
||||
except ValueError:
|
||||
break
|
||||
else:
|
||||
break
|
||||
|
||||
self._size = int(x), int(y)
|
||||
|
|
125
src/PIL/Image.py
125
src/PIL/Image.py
|
@ -1839,7 +1839,7 @@ class Image:
|
|||
Modifies the pixel at the given position. The color is given as
|
||||
a single numerical value for single-band images, and a tuple for
|
||||
multi-band images. In addition to this, RGB and RGBA tuples are
|
||||
accepted for P images.
|
||||
accepted for P and PA images.
|
||||
|
||||
Note that this method is relatively slow. For more extensive changes,
|
||||
use :py:meth:`~PIL.Image.Image.paste` or the :py:mod:`~PIL.ImageDraw`
|
||||
|
@ -1864,12 +1864,17 @@ class Image:
|
|||
return self.pyaccess.putpixel(xy, value)
|
||||
|
||||
if (
|
||||
self.mode == "P"
|
||||
self.mode in ("P", "PA")
|
||||
and isinstance(value, (list, tuple))
|
||||
and len(value) in [3, 4]
|
||||
):
|
||||
# RGB or RGBA value for a P image
|
||||
# RGB or RGBA value for a P or PA image
|
||||
if self.mode == "PA":
|
||||
alpha = value[3] if len(value) == 4 else 255
|
||||
value = value[:3]
|
||||
value = self.palette.getcolor(value, self)
|
||||
if self.mode == "PA":
|
||||
value = (value, alpha)
|
||||
return self.im.putpixel(xy, value)
|
||||
|
||||
def remap_palette(self, dest_map, source_palette=None):
|
||||
|
@ -1984,18 +1989,14 @@ class Image:
|
|||
:param size: The requested size in pixels, as a 2-tuple:
|
||||
(width, height).
|
||||
:param resample: An optional resampling filter. This can be
|
||||
one of :py:data:`PIL.Image.Resampling.NEAREST`,
|
||||
:py:data:`PIL.Image.Resampling.BOX`,
|
||||
:py:data:`PIL.Image.Resampling.BILINEAR`,
|
||||
:py:data:`PIL.Image.Resampling.HAMMING`,
|
||||
:py:data:`PIL.Image.Resampling.BICUBIC` or
|
||||
:py:data:`PIL.Image.Resampling.LANCZOS`.
|
||||
one of :py:data:`Resampling.NEAREST`, :py:data:`Resampling.BOX`,
|
||||
:py:data:`Resampling.BILINEAR`, :py:data:`Resampling.HAMMING`,
|
||||
:py:data:`Resampling.BICUBIC` or :py:data:`Resampling.LANCZOS`.
|
||||
If the image has mode "1" or "P", it is always set to
|
||||
:py:data:`PIL.Image.Resampling.NEAREST`.
|
||||
If the image mode specifies a number of bits, such as "I;16", then the
|
||||
default filter is :py:data:`PIL.Image.Resampling.NEAREST`.
|
||||
Otherwise, the default filter is
|
||||
:py:data:`PIL.Image.Resampling.BICUBIC`. See: :ref:`concept-filters`.
|
||||
:py:data:`Resampling.NEAREST`. If the image mode specifies a number
|
||||
of bits, such as "I;16", then the default filter is
|
||||
:py:data:`Resampling.NEAREST`. Otherwise, the default filter is
|
||||
:py:data:`Resampling.BICUBIC`. See: :ref:`concept-filters`.
|
||||
:param box: An optional 4-tuple of floats providing
|
||||
the source image region to be scaled.
|
||||
The values must be within (0, 0, width, height) rectangle.
|
||||
|
@ -2135,12 +2136,12 @@ class Image:
|
|||
|
||||
:param angle: In degrees counter clockwise.
|
||||
:param resample: An optional resampling filter. This can be
|
||||
one of :py:data:`PIL.Image.Resampling.NEAREST` (use nearest neighbour),
|
||||
:py:data:`PIL.Image.BILINEAR` (linear interpolation in a 2x2
|
||||
environment), or :py:data:`PIL.Image.Resampling.BICUBIC`
|
||||
(cubic spline interpolation in a 4x4 environment).
|
||||
If omitted, or if the image has mode "1" or "P", it is
|
||||
set to :py:data:`PIL.Image.Resampling.NEAREST`. See :ref:`concept-filters`.
|
||||
one of :py:data:`Resampling.NEAREST` (use nearest neighbour),
|
||||
:py:data:`Resampling.BILINEAR` (linear interpolation in a 2x2
|
||||
environment), or :py:data:`Resampling.BICUBIC` (cubic spline
|
||||
interpolation in a 4x4 environment). If omitted, or if the image has
|
||||
mode "1" or "P", it is set to :py:data:`Resampling.NEAREST`.
|
||||
See :ref:`concept-filters`.
|
||||
:param expand: Optional expansion flag. If true, expands the output
|
||||
image to make it large enough to hold the entire rotated image.
|
||||
If false or omitted, make the output image the same size as the
|
||||
|
@ -2447,14 +2448,11 @@ class Image:
|
|||
|
||||
:param size: Requested size.
|
||||
:param resample: Optional resampling filter. This can be one
|
||||
of :py:data:`PIL.Image.Resampling.NEAREST`,
|
||||
:py:data:`PIL.Image.Resampling.BOX`,
|
||||
:py:data:`PIL.Image.Resampling.BILINEAR`,
|
||||
:py:data:`PIL.Image.Resampling.HAMMING`,
|
||||
:py:data:`PIL.Image.Resampling.BICUBIC` or
|
||||
:py:data:`PIL.Image.Resampling.LANCZOS`.
|
||||
If omitted, it defaults to :py:data:`PIL.Image.Resampling.BICUBIC`.
|
||||
(was :py:data:`PIL.Image.Resampling.NEAREST` prior to version 2.5.0).
|
||||
of :py:data:`Resampling.NEAREST`, :py:data:`Resampling.BOX`,
|
||||
:py:data:`Resampling.BILINEAR`, :py:data:`Resampling.HAMMING`,
|
||||
:py:data:`Resampling.BICUBIC` or :py:data:`Resampling.LANCZOS`.
|
||||
If omitted, it defaults to :py:data:`Resampling.BICUBIC`.
|
||||
(was :py:data:`Resampling.NEAREST` prior to version 2.5.0).
|
||||
See: :ref:`concept-filters`.
|
||||
:param reducing_gap: Apply optimization by resizing the image
|
||||
in two steps. First, reducing the image by integer times
|
||||
|
@ -2473,29 +2471,41 @@ class Image:
|
|||
:returns: None
|
||||
"""
|
||||
|
||||
self.load()
|
||||
x, y = map(math.floor, size)
|
||||
if x >= self.width and y >= self.height:
|
||||
return
|
||||
provided_size = tuple(map(math.floor, size))
|
||||
|
||||
def round_aspect(number, key):
|
||||
return max(min(math.floor(number), math.ceil(number), key=key), 1)
|
||||
def preserve_aspect_ratio():
|
||||
def round_aspect(number, key):
|
||||
return max(min(math.floor(number), math.ceil(number), key=key), 1)
|
||||
|
||||
# preserve aspect ratio
|
||||
aspect = self.width / self.height
|
||||
if x / y >= aspect:
|
||||
x = round_aspect(y * aspect, key=lambda n: abs(aspect - n / y))
|
||||
else:
|
||||
y = round_aspect(
|
||||
x / aspect, key=lambda n: 0 if n == 0 else abs(aspect - x / n)
|
||||
)
|
||||
size = (x, y)
|
||||
x, y = provided_size
|
||||
if x >= self.width and y >= self.height:
|
||||
return
|
||||
|
||||
aspect = self.width / self.height
|
||||
if x / y >= aspect:
|
||||
x = round_aspect(y * aspect, key=lambda n: abs(aspect - n / y))
|
||||
else:
|
||||
y = round_aspect(
|
||||
x / aspect, key=lambda n: 0 if n == 0 else abs(aspect - x / n)
|
||||
)
|
||||
return x, y
|
||||
|
||||
box = None
|
||||
if reducing_gap is not None:
|
||||
size = preserve_aspect_ratio()
|
||||
if size is None:
|
||||
return
|
||||
|
||||
res = self.draft(None, (size[0] * reducing_gap, size[1] * reducing_gap))
|
||||
if res is not None:
|
||||
box = res[1]
|
||||
if box is None:
|
||||
self.load()
|
||||
|
||||
# load() may have changed the size of the image
|
||||
size = preserve_aspect_ratio()
|
||||
if size is None:
|
||||
return
|
||||
|
||||
if self.size != size:
|
||||
im = self.resize(size, resample, box=box, reducing_gap=reducing_gap)
|
||||
|
@ -2525,11 +2535,11 @@ class Image:
|
|||
|
||||
:param size: The output size.
|
||||
:param method: The transformation method. This is one of
|
||||
:py:data:`PIL.Image.Transform.EXTENT` (cut out a rectangular subregion),
|
||||
:py:data:`PIL.Image.Transform.AFFINE` (affine transform),
|
||||
:py:data:`PIL.Image.Transform.PERSPECTIVE` (perspective transform),
|
||||
:py:data:`PIL.Image.Transform.QUAD` (map a quadrilateral to a rectangle), or
|
||||
:py:data:`PIL.Image.Transform.MESH` (map a number of source quadrilaterals
|
||||
:py:data:`Transform.EXTENT` (cut out a rectangular subregion),
|
||||
:py:data:`Transform.AFFINE` (affine transform),
|
||||
:py:data:`Transform.PERSPECTIVE` (perspective transform),
|
||||
:py:data:`Transform.QUAD` (map a quadrilateral to a rectangle), or
|
||||
:py:data:`Transform.MESH` (map a number of source quadrilaterals
|
||||
in one operation).
|
||||
|
||||
It may also be an :py:class:`~PIL.Image.ImageTransformHandler`
|
||||
|
@ -2549,11 +2559,11 @@ class Image:
|
|||
return method, data
|
||||
:param data: Extra data to the transformation method.
|
||||
:param resample: Optional resampling filter. It can be one of
|
||||
:py:data:`PIL.Image.Resampling.NEAREST` (use nearest neighbour),
|
||||
:py:data:`PIL.Image.Resampling.BILINEAR` (linear interpolation in a 2x2
|
||||
environment), or :py:data:`PIL.Image.BICUBIC` (cubic spline
|
||||
:py:data:`Resampling.NEAREST` (use nearest neighbour),
|
||||
:py:data:`Resampling.BILINEAR` (linear interpolation in a 2x2
|
||||
environment), or :py:data:`Resampling.BICUBIC` (cubic spline
|
||||
interpolation in a 4x4 environment). If omitted, or if the image
|
||||
has mode "1" or "P", it is set to :py:data:`PIL.Image.Resampling.NEAREST`.
|
||||
has mode "1" or "P", it is set to :py:data:`Resampling.NEAREST`.
|
||||
See: :ref:`concept-filters`.
|
||||
:param fill: If ``method`` is an
|
||||
:py:class:`~PIL.Image.ImageTransformHandler` object, this is one of
|
||||
|
@ -2680,13 +2690,10 @@ class Image:
|
|||
"""
|
||||
Transpose image (flip or rotate in 90 degree steps)
|
||||
|
||||
:param method: One of :py:data:`PIL.Image.Transpose.FLIP_LEFT_RIGHT`,
|
||||
:py:data:`PIL.Image.Transpose.FLIP_TOP_BOTTOM`,
|
||||
:py:data:`PIL.Image.Transpose.ROTATE_90`,
|
||||
:py:data:`PIL.Image.Transpose.ROTATE_180`,
|
||||
:py:data:`PIL.Image.Transpose.ROTATE_270`,
|
||||
:py:data:`PIL.Image.Transpose.TRANSPOSE` or
|
||||
:py:data:`PIL.Image.Transpose.TRANSVERSE`.
|
||||
:param method: One of :py:data:`Transpose.FLIP_LEFT_RIGHT`,
|
||||
:py:data:`Transpose.FLIP_TOP_BOTTOM`, :py:data:`Transpose.ROTATE_90`,
|
||||
:py:data:`Transpose.ROTATE_180`, :py:data:`Transpose.ROTATE_270`,
|
||||
:py:data:`Transpose.TRANSPOSE` or :py:data:`Transpose.TRANSVERSE`.
|
||||
:returns: Returns a flipped or rotated copy of this image.
|
||||
"""
|
||||
|
||||
|
|
|
@ -192,6 +192,9 @@ class ImageFile(Image.Image):
|
|||
|
||||
with open(self.filename) as fp:
|
||||
self.map = mmap.mmap(fp.fileno(), 0, access=mmap.ACCESS_READ)
|
||||
if offset + self.size[1] * args[1] > self.map.size():
|
||||
# buffer is not large enough
|
||||
raise OSError
|
||||
self.im = Image.core.map_buffer(
|
||||
self.map, self.size, decoder_name, offset, args
|
||||
)
|
||||
|
|
|
@ -68,21 +68,7 @@ def _pyimagingtkcall(command, photo, id):
|
|||
# may raise an error if it cannot attach to Tkinter
|
||||
from . import _imagingtk
|
||||
|
||||
try:
|
||||
if hasattr(tk, "interp"):
|
||||
# Required for PyPy, which always has CFFI installed
|
||||
from cffi import FFI
|
||||
|
||||
ffi = FFI()
|
||||
|
||||
# PyPy is using an FFI CDATA element
|
||||
# (Pdb) self.tk.interp
|
||||
# <cdata 'Tcl_Interp *' 0x3061b50>
|
||||
_imagingtk.tkinit(int(ffi.cast("uintptr_t", tk.interp)), 1)
|
||||
else:
|
||||
_imagingtk.tkinit(tk.interpaddr(), 1)
|
||||
except AttributeError:
|
||||
_imagingtk.tkinit(id(tk), 0)
|
||||
_imagingtk.tkinit(tk.interpaddr())
|
||||
tk.call(command, photo, id)
|
||||
|
||||
|
||||
|
|
|
@ -25,7 +25,7 @@ import math
|
|||
import os
|
||||
import time
|
||||
|
||||
from . import Image, ImageFile, ImageSequence, PdfParser, __version__
|
||||
from . import Image, ImageFile, ImageSequence, PdfParser, __version__, features
|
||||
|
||||
#
|
||||
# --------------------------------------------------------------------
|
||||
|
@ -130,20 +130,23 @@ def _save(im, fp, filename, save_all=False):
|
|||
width, height = im.size
|
||||
|
||||
if im.mode == "1":
|
||||
filter = "CCITTFaxDecode"
|
||||
bits = 1
|
||||
params = PdfParser.PdfArray(
|
||||
[
|
||||
PdfParser.PdfDict(
|
||||
{
|
||||
"K": -1,
|
||||
"BlackIs1": True,
|
||||
"Columns": width,
|
||||
"Rows": height,
|
||||
}
|
||||
)
|
||||
]
|
||||
)
|
||||
if features.check("libtiff"):
|
||||
filter = "CCITTFaxDecode"
|
||||
bits = 1
|
||||
params = PdfParser.PdfArray(
|
||||
[
|
||||
PdfParser.PdfDict(
|
||||
{
|
||||
"K": -1,
|
||||
"BlackIs1": True,
|
||||
"Columns": width,
|
||||
"Rows": height,
|
||||
}
|
||||
)
|
||||
]
|
||||
)
|
||||
else:
|
||||
filter = "DCTDecode"
|
||||
colorspace = PdfParser.PdfName("DeviceGray")
|
||||
procset = "ImageB" # grayscale
|
||||
elif im.mode == "L":
|
||||
|
|
|
@ -58,7 +58,7 @@ class PyAccess:
|
|||
|
||||
# Keep pointer to im object to prevent dereferencing.
|
||||
self._im = img.im
|
||||
if self._im.mode == "P":
|
||||
if self._im.mode in ("P", "PA"):
|
||||
self._palette = img.palette
|
||||
|
||||
# Debugging is polluting test traces, only useful here
|
||||
|
@ -89,12 +89,17 @@ class PyAccess:
|
|||
(x, y) = self.check_xy((x, y))
|
||||
|
||||
if (
|
||||
self._im.mode == "P"
|
||||
self._im.mode in ("P", "PA")
|
||||
and isinstance(color, (list, tuple))
|
||||
and len(color) in [3, 4]
|
||||
):
|
||||
# RGB or RGBA value for a P image
|
||||
# RGB or RGBA value for a P or PA image
|
||||
if self._im.mode == "PA":
|
||||
alpha = color[3] if len(color) == 4 else 255
|
||||
color = color[:3]
|
||||
color = self._palette.getcolor(color, self._img)
|
||||
if self._im.mode == "PA":
|
||||
color = (color, alpha)
|
||||
|
||||
return self.set_pixel(x, y, color)
|
||||
|
||||
|
|
|
@ -193,9 +193,10 @@ def _save(im, fp, filename):
|
|||
warnings.warn("id_section has been trimmed to 255 characters")
|
||||
|
||||
if colormaptype:
|
||||
colormapfirst, colormaplength, colormapentry = 0, 256, 24
|
||||
palette = im.im.getpalette("RGB", "BGR")
|
||||
colormaplength, colormapentry = len(palette) // 3, 24
|
||||
else:
|
||||
colormapfirst, colormaplength, colormapentry = 0, 0, 0
|
||||
colormaplength, colormapentry = 0, 0
|
||||
|
||||
if im.mode in ("LA", "RGBA"):
|
||||
flags = 8
|
||||
|
@ -210,7 +211,7 @@ def _save(im, fp, filename):
|
|||
o8(id_len)
|
||||
+ o8(colormaptype)
|
||||
+ o8(imagetype)
|
||||
+ o16(colormapfirst)
|
||||
+ o16(0) # colormapfirst
|
||||
+ o16(colormaplength)
|
||||
+ o8(colormapentry)
|
||||
+ o16(0)
|
||||
|
@ -225,7 +226,7 @@ def _save(im, fp, filename):
|
|||
fp.write(id_section)
|
||||
|
||||
if colormaptype:
|
||||
fp.write(im.im.getpalette("RGB", "BGR"))
|
||||
fp.write(palette)
|
||||
|
||||
if rle:
|
||||
ImageFile._save(
|
||||
|
|
|
@ -727,7 +727,9 @@ class ImageFileDirectory_v2(MutableMapping):
|
|||
@_register_writer(2)
|
||||
def write_string(self, value):
|
||||
# remerge of https://github.com/python-pillow/Pillow/pull/1416
|
||||
return b"" + value.encode("ascii", "replace") + b"\0"
|
||||
if not isinstance(value, bytes):
|
||||
value = value.encode("ascii", "replace")
|
||||
return value + b"\0"
|
||||
|
||||
@_register_loader(5, 8)
|
||||
def load_rational(self, data, legacy_api=True):
|
||||
|
@ -1153,7 +1155,7 @@ class TiffImageFile(ImageFile.ImageFile):
|
|||
|
||||
:returns: XMP tags in a dictionary.
|
||||
"""
|
||||
return self._getxmp(self.tag_v2[700]) if 700 in self.tag_v2 else {}
|
||||
return self._getxmp(self.tag_v2[XMP]) if XMP in self.tag_v2 else {}
|
||||
|
||||
def get_photoshop_blocks(self):
|
||||
"""
|
||||
|
@ -1328,7 +1330,7 @@ class TiffImageFile(ImageFile.ImageFile):
|
|||
logger.debug(f"- photometric_interpretation: {photo}")
|
||||
logger.debug(f"- planar_configuration: {self._planar_configuration}")
|
||||
logger.debug(f"- fill_order: {fillorder}")
|
||||
logger.debug(f"- YCbCr subsampling: {self.tag.get(530)}")
|
||||
logger.debug(f"- YCbCr subsampling: {self.tag.get(YCBCRSUBSAMPLING)}")
|
||||
|
||||
# size
|
||||
xsize = int(self.tag_v2.get(IMAGEWIDTH))
|
||||
|
@ -1469,8 +1471,8 @@ class TiffImageFile(ImageFile.ImageFile):
|
|||
else:
|
||||
# tiled image
|
||||
offsets = self.tag_v2[TILEOFFSETS]
|
||||
w = self.tag_v2.get(322)
|
||||
h = self.tag_v2.get(323)
|
||||
w = self.tag_v2.get(TILEWIDTH)
|
||||
h = self.tag_v2.get(TILELENGTH)
|
||||
|
||||
for offset in offsets:
|
||||
if x + w > xsize:
|
||||
|
|
|
@ -23,33 +23,16 @@ TkImaging_Init(Tcl_Interp *interp);
|
|||
extern int
|
||||
load_tkinter_funcs(void);
|
||||
|
||||
/* copied from _tkinter.c (this isn't as bad as it may seem: for new
|
||||
versions, we use _tkinter's interpaddr hook instead, and all older
|
||||
versions use this structure layout) */
|
||||
|
||||
typedef struct {
|
||||
PyObject_HEAD Tcl_Interp *interp;
|
||||
} TkappObject;
|
||||
|
||||
static PyObject *
|
||||
_tkinit(PyObject *self, PyObject *args) {
|
||||
Tcl_Interp *interp;
|
||||
|
||||
PyObject *arg;
|
||||
int is_interp;
|
||||
if (!PyArg_ParseTuple(args, "Oi", &arg, &is_interp)) {
|
||||
if (!PyArg_ParseTuple(args, "O", &arg)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (is_interp) {
|
||||
interp = (Tcl_Interp *)PyLong_AsVoidPtr(arg);
|
||||
} else {
|
||||
TkappObject *app;
|
||||
/* Do it the hard way. This will break if the TkappObject
|
||||
layout changes */
|
||||
app = (TkappObject *)PyLong_AsVoidPtr(arg);
|
||||
interp = app->interp;
|
||||
}
|
||||
interp = (Tcl_Interp *)PyLong_AsVoidPtr(arg);
|
||||
|
||||
/* This will bomb if interp is invalid... */
|
||||
TkImaging_Init(interp);
|
||||
|
|
|
@ -1243,7 +1243,7 @@ frompalette(Imaging imOut, Imaging imIn, const char *mode) {
|
|||
if (!imOut) {
|
||||
return NULL;
|
||||
}
|
||||
if (strcmp(mode, "P") == 0) {
|
||||
if (strcmp(mode, "P") == 0 || strcmp(mode, "PA") == 0) {
|
||||
ImagingPaletteDelete(imOut->palette);
|
||||
imOut->palette = ImagingPaletteDuplicate(imIn->palette);
|
||||
}
|
||||
|
|
|
@ -916,7 +916,7 @@ ImagingLibTiffEncode(Imaging im, ImagingCodecState state, UINT8 *buffer, int byt
|
|||
dump_state(clientstate);
|
||||
|
||||
if (state->state == 0) {
|
||||
TRACE(("Encoding line bt line"));
|
||||
TRACE(("Encoding line by line"));
|
||||
while (state->y < state->ysize) {
|
||||
state->shuffle(
|
||||
state->buffer,
|
||||
|
|
|
@ -11,8 +11,8 @@ For more extensive info, see the [Windows build instructions](build.rst).
|
|||
* Requires Microsoft Visual Studio 2017 or newer with C++ component.
|
||||
* Requires NASM for libjpeg-turbo, a required dependency when using this script.
|
||||
* Requires CMake 3.12 or newer (available as Visual Studio component).
|
||||
* Tested on Windows Server 2016 with Visual Studio 2017 Community (AppVeyor).
|
||||
* Tested on Windows Server 2019 with Visual Studio 2019 Enterprise (GitHub Actions).
|
||||
* Tested on Windows Server 2016 with Visual Studio 2017 Community, and Windows Server 2019 with Visual Studio 2022 Community (AppVeyor).
|
||||
* Tested on Windows Server 2022 with Visual Studio 2022 Enterprise (GitHub Actions).
|
||||
|
||||
The following is a simplified version of the script used on AppVeyor:
|
||||
```
|
||||
|
|
Loading…
Reference in New Issue
Block a user