diff --git a/.ci/install.sh b/.ci/install.sh index 17c349ab1..6e87d386d 100755 --- a/.ci/install.sh +++ b/.ci/install.sh @@ -22,7 +22,8 @@ set -e if [[ $(uname) != CYGWIN* ]]; then sudo apt-get -qq install libfreetype6-dev liblcms2-dev python3-tk\ ghostscript libffi-dev libjpeg-turbo-progs libopenjp2-7-dev\ - cmake meson imagemagick libharfbuzz-dev libfribidi-dev + cmake meson imagemagick libharfbuzz-dev libfribidi-dev\ + sway wl-clipboard fi python3 -m pip install --upgrade pip @@ -41,7 +42,7 @@ if [[ $(uname) != CYGWIN* ]]; then if ! [ "$GHA_PYTHON_VERSION" == "3.12-dev" ]; then python3 -m pip install numpy ; fi # PyQt6 doesn't support PyPy3 - if [[ $GHA_PYTHON_VERSION == 3.* ]]; then + if [[ "$GHA_PYTHON_VERSION" != "3.12-dev" && $GHA_PYTHON_VERSION == 3.* ]]; then sudo apt-get -qq install libegl1 libxcb-cursor0 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-shape0 libxkbcommon-x11-0 python3 -m pip install pyqt6 fi diff --git a/.editorconfig b/.editorconfig index 449530717..d74549fe2 100644 --- a/.editorconfig +++ b/.editorconfig @@ -13,10 +13,6 @@ indent_style = space trim_trailing_whitespace = true -[*.rst] -# Four-space indentation -indent_size = 4 - [*.yml] # Two-space indentation indent_size = 2 diff --git a/.github/workflows/test-docker.yml b/.github/workflows/test-docker.yml index 4f01abe44..3bcb8cfbc 100644 --- a/.github/workflows/test-docker.yml +++ b/.github/workflows/test-docker.yml @@ -39,6 +39,7 @@ jobs: centos-stream-8-amd64, centos-stream-9-amd64, debian-11-bullseye-x86, + debian-12-bookworm-x86, fedora-37-amd64, fedora-38-amd64, gentoo, diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index a00880111..076b80839 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -65,8 +65,8 @@ jobs: - name: Print build system information run: python3 .github/workflows/system-info.py - - name: python3 -m pip install wheel pytest pytest-cov pytest-timeout defusedxml - run: python3 -m pip install wheel pytest pytest-cov pytest-timeout defusedxml + - name: python3 -m pip install setuptools wheel pytest pytest-cov pytest-timeout defusedxml + run: python3 -m pip install setuptools wheel pytest pytest-cov pytest-timeout defusedxml - name: Install dependencies id: install diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index fced6113b..afb8fb56c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -84,7 +84,9 @@ jobs: python3 -m pip install pytest-reverse fi if [ "${{ matrix.os }}" = "ubuntu-latest" ]; then - xvfb-run -s '-screen 0 1024x768x24' .ci/test.sh + xvfb-run -s '-screen 0 1024x768x24' sway& + export WAYLAND_DISPLAY=wayland-1 + .ci/test.sh else .ci/test.sh fi diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4882a317f..0ddc6beb4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -4,9 +4,6 @@ repos: hooks: - id: black args: [--target-version=py38] - # Only .py files, until https://github.com/psf/black/issues/402 resolved - files: \.py$ - types: [] - repo: https://github.com/PyCQA/isort rev: 5.12.0 diff --git a/.readthedocs.yml b/.readthedocs.yml index ec3300dd1..bda03d944 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -1,6 +1,6 @@ version: 2 -formats: all +formats: [pdf] build: os: ubuntu-22.04 diff --git a/CHANGES.rst b/CHANGES.rst index 7b13900a8..c51f8fb94 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,57 @@ Changelog (Pillow) 10.0.0 (unreleased) ------------------- +- Fixed combining single duration across duplicate APNG frames #7146 + [radarhere] + +- Remove temporary file when error is raised #7148 + [radarhere] + +- Do not use temporary file when grabbing clipboard on Linux #7200 + [radarhere] + +- If the clipboard fails to open on Windows, wait and try again #7141 + [radarhere] + +- Fixed saving multiple 1 mode frames to GIF #7181 + [radarhere] + +- Replaced absolute PIL import with relative import #7173 + [radarhere] + +- Replaced deprecated Py_FileSystemDefaultEncoding for Python >= 3.12 #7192 + [radarhere] + +- Improved wl-paste mimetype handling in ImageGrab #7094 + [rrcgat, radarhere] + +- Added _repr_jpeg_() for IPython display_jpeg #7135 + [n3011, radarhere, nulano] + +- Use "/sbin/ldconfig" if ldconfig is not found #7068 + [radarhere] + +- Prefer screenshots using XCB over gnome-screenshot #7143 + [nulano, radarhere] + +- Fixed joined corners for ImageDraw rounded_rectangle() odd dimensions #7151 + [radarhere] + +- Support reading signed 8-bit TIFF images #7111 + [radarhere] + +- Added width argument to ImageDraw regular_polygon #7132 + [radarhere] + +- Support I mode for ImageFilter.BuiltinFilter #7108 + [radarhere] + +- Raise error from stderr of Linux ImageGrab.grabclipboard() command #7112 + [radarhere] + +- Added unpacker from I;16B to I;16 #7125 + [radarhere] + - Support float font sizes #7107 [radarhere] diff --git a/Tests/check_jpeg_leaks.py b/Tests/check_jpeg_leaks.py index ab8d77719..940c0b00d 100644 --- a/Tests/check_jpeg_leaks.py +++ b/Tests/check_jpeg_leaks.py @@ -75,43 +75,42 @@ post-patch: """ -def test_qtables_leak(): +standard_l_qtable = ( + # fmt: off + 16, 11, 10, 16, 24, 40, 51, 61, + 12, 12, 14, 19, 26, 58, 60, 55, + 14, 13, 16, 24, 40, 57, 69, 56, + 14, 17, 22, 29, 51, 87, 80, 62, + 18, 22, 37, 56, 68, 109, 103, 77, + 24, 35, 55, 64, 81, 104, 113, 92, + 49, 64, 78, 87, 103, 121, 120, 101, + 72, 92, 95, 98, 112, 100, 103, 99, + # fmt: on +) + +standard_chrominance_qtable = ( + # fmt: off + 17, 18, 24, 47, 99, 99, 99, 99, + 18, 21, 26, 66, 99, 99, 99, 99, + 24, 26, 56, 99, 99, 99, 99, 99, + 47, 66, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, + # fmt: on +) + + +@pytest.mark.parametrize( + "qtables", + ( + (standard_l_qtable, standard_chrominance_qtable), + [standard_l_qtable, standard_chrominance_qtable], + ), +) +def test_qtables_leak(qtables): im = hopper("RGB") - - standard_l_qtable = [ - int(s) - for s in """ - 16 11 10 16 24 40 51 61 - 12 12 14 19 26 58 60 55 - 14 13 16 24 40 57 69 56 - 14 17 22 29 51 87 80 62 - 18 22 37 56 68 109 103 77 - 24 35 55 64 81 104 113 92 - 49 64 78 87 103 121 120 101 - 72 92 95 98 112 100 103 99 - """.split( - None - ) - ] - - standard_chrominance_qtable = [ - int(s) - for s in """ - 17 18 24 47 99 99 99 99 - 18 21 26 66 99 99 99 99 - 24 26 56 99 99 99 99 99 - 47 66 99 99 99 99 99 99 - 99 99 99 99 99 99 99 99 - 99 99 99 99 99 99 99 99 - 99 99 99 99 99 99 99 99 - 99 99 99 99 99 99 99 99 - """.split( - None - ) - ] - - qtables = [standard_l_qtable, standard_chrominance_qtable] - for _ in range(iterations): test_output = BytesIO() im.save(test_output, "JPEG", qtables=qtables) diff --git a/Tests/images/8bit.s.tif b/Tests/images/8bit.s.tif new file mode 100644 index 000000000..043cba6af Binary files /dev/null and b/Tests/images/8bit.s.tif differ diff --git a/Tests/images/hopper_emboss_I.png b/Tests/images/hopper_emboss_I.png new file mode 100644 index 000000000..f4dab388f Binary files /dev/null and b/Tests/images/hopper_emboss_I.png differ diff --git a/Tests/images/hopper_emboss_more_I.png b/Tests/images/hopper_emboss_more_I.png new file mode 100644 index 000000000..c417c915f Binary files /dev/null and b/Tests/images/hopper_emboss_more_I.png differ diff --git a/Tests/images/imagedraw_rounded_rectangle_x_odd.png b/Tests/images/imagedraw_rounded_rectangle_x_odd.png new file mode 100644 index 000000000..f23f1945e Binary files /dev/null and b/Tests/images/imagedraw_rounded_rectangle_x_odd.png differ diff --git a/Tests/images/imagedraw_rounded_rectangle_y_odd.png b/Tests/images/imagedraw_rounded_rectangle_y_odd.png new file mode 100644 index 000000000..96441bc72 Binary files /dev/null and b/Tests/images/imagedraw_rounded_rectangle_y_odd.png differ diff --git a/Tests/images/imagedraw_triangle_width.png b/Tests/images/imagedraw_triangle_width.png new file mode 100644 index 000000000..3d35326e7 Binary files /dev/null and b/Tests/images/imagedraw_triangle_width.png differ diff --git a/Tests/test_file_apng.py b/Tests/test_file_apng.py index cf1312b77..8cb9a814e 100644 --- a/Tests/test_file_apng.py +++ b/Tests/test_file_apng.py @@ -461,6 +461,17 @@ def test_apng_save_duration_loop(tmp_path): assert im.info.get("duration") == 750 +def test_apng_save_duplicate_duration(tmp_path): + test_file = str(tmp_path / "temp.png") + frame = Image.new("RGB", (1, 1)) + + # Test a single duration is correctly combined across duplicate frames + frame.save(test_file, save_all=True, append_images=[frame, frame], duration=500) + with Image.open(test_file) as im: + assert im.n_frames == 1 + assert im.info.get("duration") == 1500 + + def test_apng_save_disposal(tmp_path): test_file = str(tmp_path / "temp.png") size = (128, 64) diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index 8522f486a..0e50ee1ab 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -252,6 +252,19 @@ def test_roundtrip_save_all(tmp_path): assert reread.n_frames == 5 +def test_roundtrip_save_all_1(tmp_path): + out = str(tmp_path / "temp.gif") + im = Image.new("1", (1, 1)) + im2 = Image.new("1", (1, 1), 1) + im.save(out, save_all=True, append_images=[im2]) + + with Image.open(out) as reloaded: + assert reloaded.getpixel((0, 0)) == 0 + + reloaded.seek(1) + assert reloaded.getpixel((0, 0)) == 255 + + @pytest.mark.parametrize( "path, mode", ( diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index 73a00386f..0247527f5 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -922,6 +922,19 @@ class TestFileJpeg: im.load() ImageFile.LOAD_TRUNCATED_IMAGES = False + def test_repr_jpeg(self): + im = hopper() + + with Image.open(BytesIO(im._repr_jpeg_())) as repr_jpeg: + assert repr_jpeg.format == "JPEG" + assert_image_similar(im, repr_jpeg, 17) + + def test_repr_jpeg_error(self): + im = hopper("F") + + with pytest.raises(ValueError): + im._repr_jpeg_() + @pytest.mark.skipif(not is_win32(), reason="Windows only") @skip_unless_feature("jpg") diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index 97a02ac96..f13436ce8 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -96,10 +96,17 @@ class TestFileTiff: assert_image_similar_tofile(im, "Tests/images/pil136.png", 1) - def test_bigtiff(self): + def test_bigtiff(self, tmp_path): with Image.open("Tests/images/hopper_bigtiff.tif") as im: assert_image_equal_tofile(im, "Tests/images/hopper.tif") + with Image.open("Tests/images/hopper_bigtiff.tif") as im: + # multistrip support not yet implemented + del im.tag_v2[273] + + outfile = str(tmp_path / "temp.tif") + im.save(outfile, save_all=True, append_images=[im], tiffinfo=im.tag_v2) + def test_set_legacy_api(self): ifd = TiffImagePlugin.ImageFileDirectory_v2() with pytest.raises(Exception) as e: @@ -198,6 +205,12 @@ class TestFileTiff: with pytest.raises(OSError): im.save(outfile) + def test_8bit_s(self): + with Image.open("Tests/images/8bit.s.tif") as im: + im.load() + assert im.mode == "L" + assert im.getpixel((50, 50)) == 184 + def test_little_endian(self): with Image.open("Tests/images/16bit.cropped.tif") as im: assert im.getpixel((0, 0)) == 480 diff --git a/Tests/test_image_filter.py b/Tests/test_image_filter.py index a2ef2280b..25b72298e 100644 --- a/Tests/test_image_filter.py +++ b/Tests/test_image_filter.py @@ -30,15 +30,16 @@ from .helper import assert_image_equal, hopper ImageFilter.UnsharpMask(10), ), ) -@pytest.mark.parametrize("mode", ("L", "RGB", "CMYK")) +@pytest.mark.parametrize("mode", ("L", "I", "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 + if mode != "I" or isinstance(filter_to_apply, ImageFilter.BuiltinFilter): + out = im.filter(filter_to_apply) + assert out.mode == im.mode + assert out.size == im.size -@pytest.mark.parametrize("mode", ("L", "RGB", "CMYK")) +@pytest.mark.parametrize("mode", ("L", "I", "RGB", "CMYK")) def test_sanity_error(mode): with pytest.raises(TypeError): im = hopper(mode) @@ -130,10 +131,12 @@ def test_kernel_not_enough_coefficients(): ImageFilter.Kernel((3, 3), (0, 0)) -@pytest.mark.parametrize("mode", ("L", "LA", "RGB", "CMYK")) +@pytest.mark.parametrize("mode", ("L", "LA", "I", "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: + reference_name = "hopper_emboss" + reference_name += "_I.png" if mode == "I" else ".bmp" + with Image.open("Tests/images/" + reference_name) as reference: kernel = ImageFilter.Kernel( (3, 3), # fmt: off @@ -146,16 +149,20 @@ def test_consistency_3x3(mode): source = source.split() * 2 reference = reference.split() * 2 - assert_image_equal( - Image.merge(mode, source[: len(mode)]).filter(kernel), - Image.merge(mode, reference[: len(mode)]), - ) + if mode == "I": + source = source[0].convert(mode) + else: + source = Image.merge(mode, source[: len(mode)]) + reference = Image.merge(mode, reference[: len(mode)]) + assert_image_equal(source.filter(kernel), reference) -@pytest.mark.parametrize("mode", ("L", "LA", "RGB", "CMYK")) +@pytest.mark.parametrize("mode", ("L", "LA", "I", "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: + reference_name = "hopper_emboss_more" + reference_name += "_I.png" if mode == "I" else ".bmp" + with Image.open("Tests/images/" + reference_name) as reference: kernel = ImageFilter.Kernel( (5, 5), # fmt: off @@ -170,10 +177,12 @@ def test_consistency_5x5(mode): source = source.split() * 2 reference = reference.split() * 2 - assert_image_equal( - Image.merge(mode, source[: len(mode)]).filter(kernel), - Image.merge(mode, reference[: len(mode)]), - ) + if mode == "I": + source = source[0].convert(mode) + else: + source = Image.merge(mode, source[: len(mode)]) + reference = Image.merge(mode, reference[: len(mode)]) + assert_image_equal(source.filter(kernel), reference) def test_invalid_box_blur_filter(): diff --git a/Tests/test_image_putpalette.py b/Tests/test_image_putpalette.py index 3b29769a7..665e08a7e 100644 --- a/Tests/test_image_putpalette.py +++ b/Tests/test_image_putpalette.py @@ -32,6 +32,14 @@ def test_putpalette(): with pytest.raises(ValueError): palette("YCbCr") + with Image.open("Tests/images/hopper_gray.jpg") as im: + assert im.mode == "L" + im.putpalette(list(range(256)) * 3) + + with Image.open("Tests/images/la.tga") as im: + assert im.mode == "LA" + im.putpalette(list(range(256)) * 3) + def test_imagepalette(): im = hopper("P") diff --git a/Tests/test_imagedraw.py b/Tests/test_imagedraw.py index 7ffd7969d..7497fdc66 100644 --- a/Tests/test_imagedraw.py +++ b/Tests/test_imagedraw.py @@ -27,15 +27,21 @@ X1 = int(X0 * 3) Y0 = int(H / 4) Y1 = int(X0 * 3) -# Two kinds of bounding box -BBOX1 = [(X0, Y0), (X1, Y1)] -BBOX2 = [X0, Y0, X1, Y1] +# Bounding boxes +BBOX = (((X0, Y0), (X1, Y1)), [(X0, Y0), (X1, Y1)], (X0, Y0, X1, Y1), [X0, Y0, X1, Y1]) -# Two kinds of coordinate sequences -POINTS1 = [(10, 10), (20, 40), (30, 30)] -POINTS2 = [10, 10, 20, 40, 30, 30] +# Coordinate sequences +POINTS = ( + ((10, 10), (20, 40), (30, 30)), + [(10, 10), (20, 40), (30, 30)], + (10, 10, 20, 40, 30, 30), + [10, 10, 20, 40, 30, 30], +) -KITE_POINTS = [(10, 50), (70, 10), (90, 50), (70, 90), (10, 50)] +KITE_POINTS = ( + ((10, 50), (70, 10), (90, 50), (70, 90), (10, 50)), + [(10, 50), (70, 10), (90, 50), (70, 90), (10, 50)], +) def test_sanity(): @@ -63,7 +69,7 @@ def test_mode_mismatch(): ImageDraw.ImageDraw(im, mode="L") -@pytest.mark.parametrize("bbox", (BBOX1, BBOX2)) +@pytest.mark.parametrize("bbox", BBOX) @pytest.mark.parametrize("start, end", ((0, 180), (0.5, 180.4))) def test_arc(bbox, start, end): # Arrange @@ -77,7 +83,8 @@ def test_arc(bbox, start, end): assert_image_similar_tofile(im, "Tests/images/imagedraw_arc.png", 1) -def test_arc_end_le_start(): +@pytest.mark.parametrize("bbox", BBOX) +def test_arc_end_le_start(bbox): # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -85,13 +92,14 @@ def test_arc_end_le_start(): end = 0 # Act - draw.arc(BBOX1, start=start, end=end) + draw.arc(bbox, start=start, end=end) # Assert assert_image_equal_tofile(im, "Tests/images/imagedraw_arc_end_le_start.png") -def test_arc_no_loops(): +@pytest.mark.parametrize("bbox", BBOX) +def test_arc_no_loops(bbox): # No need to go in loops # Arrange im = Image.new("RGB", (W, H)) @@ -100,57 +108,61 @@ def test_arc_no_loops(): end = 370 # Act - draw.arc(BBOX1, start=start, end=end) + draw.arc(bbox, start=start, end=end) # Assert assert_image_similar_tofile(im, "Tests/images/imagedraw_arc_no_loops.png", 1) -def test_arc_width(): +@pytest.mark.parametrize("bbox", BBOX) +def test_arc_width(bbox): # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) # Act - draw.arc(BBOX1, 10, 260, width=5) + draw.arc(bbox, 10, 260, width=5) # Assert assert_image_similar_tofile(im, "Tests/images/imagedraw_arc_width.png", 1) -def test_arc_width_pieslice_large(): +@pytest.mark.parametrize("bbox", BBOX) +def test_arc_width_pieslice_large(bbox): # Tests an arc with a large enough width that it is a pieslice # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) # Act - draw.arc(BBOX1, 10, 260, fill="yellow", width=100) + draw.arc(bbox, 10, 260, fill="yellow", width=100) # Assert assert_image_similar_tofile(im, "Tests/images/imagedraw_arc_width_pieslice.png", 1) -def test_arc_width_fill(): +@pytest.mark.parametrize("bbox", BBOX) +def test_arc_width_fill(bbox): # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) # Act - draw.arc(BBOX1, 10, 260, fill="yellow", width=5) + draw.arc(bbox, 10, 260, fill="yellow", width=5) # Assert assert_image_similar_tofile(im, "Tests/images/imagedraw_arc_width_fill.png", 1) -def test_arc_width_non_whole_angle(): +@pytest.mark.parametrize("bbox", BBOX) +def test_arc_width_non_whole_angle(bbox): # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) expected = "Tests/images/imagedraw_arc_width_non_whole_angle.png" # Act - draw.arc(BBOX1, 10, 259.5, width=5) + draw.arc(bbox, 10, 259.5, width=5) # Assert assert_image_similar_tofile(im, expected, 1) @@ -184,7 +196,7 @@ def test_bitmap(): @pytest.mark.parametrize("mode", ("RGB", "L")) -@pytest.mark.parametrize("bbox", (BBOX1, BBOX2)) +@pytest.mark.parametrize("bbox", BBOX) def test_chord(mode, bbox): # Arrange im = Image.new(mode, (W, H)) @@ -198,37 +210,40 @@ def test_chord(mode, bbox): assert_image_similar_tofile(im, expected, 1) -def test_chord_width(): +@pytest.mark.parametrize("bbox", BBOX) +def test_chord_width(bbox): # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) # Act - draw.chord(BBOX1, 10, 260, outline="yellow", width=5) + draw.chord(bbox, 10, 260, outline="yellow", width=5) # Assert assert_image_similar_tofile(im, "Tests/images/imagedraw_chord_width.png", 1) -def test_chord_width_fill(): +@pytest.mark.parametrize("bbox", BBOX) +def test_chord_width_fill(bbox): # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) # Act - draw.chord(BBOX1, 10, 260, fill="red", outline="yellow", width=5) + draw.chord(bbox, 10, 260, fill="red", outline="yellow", width=5) # Assert assert_image_similar_tofile(im, "Tests/images/imagedraw_chord_width_fill.png", 1) -def test_chord_zero_width(): +@pytest.mark.parametrize("bbox", BBOX) +def test_chord_zero_width(bbox): # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) # Act - draw.chord(BBOX1, 10, 260, fill="red", outline="yellow", width=0) + draw.chord(bbox, 10, 260, fill="red", outline="yellow", width=0) # Assert assert_image_equal_tofile(im, "Tests/images/imagedraw_chord_zero_width.png") @@ -247,7 +262,7 @@ def test_chord_too_fat(): @pytest.mark.parametrize("mode", ("RGB", "L")) -@pytest.mark.parametrize("bbox", (BBOX1, BBOX2)) +@pytest.mark.parametrize("bbox", BBOX) def test_ellipse(mode, bbox): # Arrange im = Image.new(mode, (W, H)) @@ -261,13 +276,14 @@ def test_ellipse(mode, bbox): assert_image_similar_tofile(im, expected, 1) -def test_ellipse_translucent(): +@pytest.mark.parametrize("bbox", BBOX) +def test_ellipse_translucent(bbox): # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im, "RGBA") # Act - draw.ellipse(BBOX1, fill=(0, 255, 0, 127)) + draw.ellipse(bbox, fill=(0, 255, 0, 127)) # Assert expected = "Tests/images/imagedraw_ellipse_translucent.png" @@ -297,13 +313,14 @@ def test_ellipse_symmetric(): assert_image_equal(im, im.transpose(Image.Transpose.FLIP_LEFT_RIGHT)) -def test_ellipse_width(): +@pytest.mark.parametrize("bbox", BBOX) +def test_ellipse_width(bbox): # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) # Act - draw.ellipse(BBOX1, outline="blue", width=5) + draw.ellipse(bbox, outline="blue", width=5) # Assert assert_image_similar_tofile(im, "Tests/images/imagedraw_ellipse_width.png", 1) @@ -321,25 +338,27 @@ def test_ellipse_width_large(): assert_image_similar_tofile(im, "Tests/images/imagedraw_ellipse_width_large.png", 1) -def test_ellipse_width_fill(): +@pytest.mark.parametrize("bbox", BBOX) +def test_ellipse_width_fill(bbox): # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) # Act - draw.ellipse(BBOX1, fill="green", outline="blue", width=5) + draw.ellipse(bbox, fill="green", outline="blue", width=5) # Assert assert_image_similar_tofile(im, "Tests/images/imagedraw_ellipse_width_fill.png", 1) -def test_ellipse_zero_width(): +@pytest.mark.parametrize("bbox", BBOX) +def test_ellipse_zero_width(bbox): # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) # Act - draw.ellipse(BBOX1, fill="green", outline="blue", width=0) + draw.ellipse(bbox, fill="green", outline="blue", width=0) # Assert assert_image_equal_tofile(im, "Tests/images/imagedraw_ellipse_zero_width.png") @@ -386,7 +405,7 @@ def test_ellipse_various_sizes_filled(): ) -@pytest.mark.parametrize("points", (POINTS1, POINTS2)) +@pytest.mark.parametrize("points", POINTS) def test_line(points): # Arrange im = Image.new("RGB", (W, H)) @@ -458,7 +477,7 @@ def test_transform(): assert_image_equal(im, expected) -@pytest.mark.parametrize("bbox", (BBOX1, BBOX2)) +@pytest.mark.parametrize("bbox", BBOX) @pytest.mark.parametrize("start, end", ((-92, 46), (-92.2, 46.2))) def test_pieslice(bbox, start, end): # Arrange @@ -472,38 +491,41 @@ def test_pieslice(bbox, start, end): assert_image_similar_tofile(im, "Tests/images/imagedraw_pieslice.png", 1) -def test_pieslice_width(): +@pytest.mark.parametrize("bbox", BBOX) +def test_pieslice_width(bbox): # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) # Act - draw.pieslice(BBOX1, 10, 260, outline="blue", width=5) + draw.pieslice(bbox, 10, 260, outline="blue", width=5) # Assert assert_image_similar_tofile(im, "Tests/images/imagedraw_pieslice_width.png", 1) -def test_pieslice_width_fill(): +@pytest.mark.parametrize("bbox", BBOX) +def test_pieslice_width_fill(bbox): # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) expected = "Tests/images/imagedraw_pieslice_width_fill.png" # Act - draw.pieslice(BBOX1, 10, 260, fill="white", outline="blue", width=5) + draw.pieslice(bbox, 10, 260, fill="white", outline="blue", width=5) # Assert assert_image_similar_tofile(im, expected, 1) -def test_pieslice_zero_width(): +@pytest.mark.parametrize("bbox", BBOX) +def test_pieslice_zero_width(bbox): # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) # Act - draw.pieslice(BBOX1, 10, 260, fill="white", outline="blue", width=0) + draw.pieslice(bbox, 10, 260, fill="white", outline="blue", width=0) # Assert assert_image_equal_tofile(im, "Tests/images/imagedraw_pieslice_zero_width.png") @@ -551,7 +573,7 @@ def test_pieslice_no_spikes(): assert_image_equal(im, im_pre_erase) -@pytest.mark.parametrize("points", (POINTS1, POINTS2)) +@pytest.mark.parametrize("points", POINTS) def test_point(points): # Arrange im = Image.new("RGB", (W, H)) @@ -564,7 +586,7 @@ def test_point(points): assert_image_equal_tofile(im, "Tests/images/imagedraw_point.png") -@pytest.mark.parametrize("points", (POINTS1, POINTS2)) +@pytest.mark.parametrize("points", POINTS) def test_polygon(points): # Arrange im = Image.new("RGB", (W, H)) @@ -578,7 +600,8 @@ def test_polygon(points): @pytest.mark.parametrize("mode", ("RGB", "L")) -def test_polygon_kite(mode): +@pytest.mark.parametrize("kite_points", KITE_POINTS) +def test_polygon_kite(mode, kite_points): # Test drawing lines of different gradients (dx>dy, dy>dx) and # vertical (dx==0) and horizontal (dy==0) lines # Arrange @@ -587,7 +610,7 @@ def test_polygon_kite(mode): expected = f"Tests/images/imagedraw_polygon_kite_{mode}.png" # Act - draw.polygon(KITE_POINTS, fill="blue", outline="yellow") + draw.polygon(kite_points, fill="blue", outline="yellow") # Assert assert_image_equal_tofile(im, expected) @@ -634,7 +657,7 @@ def test_polygon_translucent(): assert_image_equal_tofile(im, expected) -@pytest.mark.parametrize("bbox", (BBOX1, BBOX2)) +@pytest.mark.parametrize("bbox", BBOX) def test_rectangle(bbox): # Arrange im = Image.new("RGB", (W, H)) @@ -661,63 +684,68 @@ def test_big_rectangle(): assert_image_similar_tofile(im, "Tests/images/imagedraw_big_rectangle.png", 1) -def test_rectangle_width(): +@pytest.mark.parametrize("bbox", BBOX) +def test_rectangle_width(bbox): # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) expected = "Tests/images/imagedraw_rectangle_width.png" # Act - draw.rectangle(BBOX1, outline="green", width=5) + draw.rectangle(bbox, outline="green", width=5) # Assert assert_image_equal_tofile(im, expected) -def test_rectangle_width_fill(): +@pytest.mark.parametrize("bbox", BBOX) +def test_rectangle_width_fill(bbox): # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) expected = "Tests/images/imagedraw_rectangle_width_fill.png" # Act - draw.rectangle(BBOX1, fill="blue", outline="green", width=5) + draw.rectangle(bbox, fill="blue", outline="green", width=5) # Assert assert_image_equal_tofile(im, expected) -def test_rectangle_zero_width(): +@pytest.mark.parametrize("bbox", BBOX) +def test_rectangle_zero_width(bbox): # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) # Act - draw.rectangle(BBOX1, fill="blue", outline="green", width=0) + draw.rectangle(bbox, fill="blue", outline="green", width=0) # Assert assert_image_equal_tofile(im, "Tests/images/imagedraw_rectangle_zero_width.png") -def test_rectangle_I16(): +@pytest.mark.parametrize("bbox", BBOX) +def test_rectangle_I16(bbox): # Arrange im = Image.new("I;16", (W, H)) draw = ImageDraw.Draw(im) # Act - draw.rectangle(BBOX1, fill="black", outline="green") + draw.rectangle(bbox, fill="black", outline="green") # Assert assert_image_equal_tofile(im.convert("I"), "Tests/images/imagedraw_rectangle_I.png") -def test_rectangle_translucent_outline(): +@pytest.mark.parametrize("bbox", BBOX) +def test_rectangle_translucent_outline(bbox): # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im, "RGBA") # Act - draw.rectangle(BBOX1, fill="black", outline=(0, 255, 0, 127), width=5) + draw.rectangle(bbox, fill="black", outline=(0, 255, 0, 127), width=5) # Assert assert_image_equal_tofile( @@ -794,13 +822,14 @@ def test_rounded_rectangle_non_integer_radius(xy, radius, type): ) -def test_rounded_rectangle_zero_radius(): +@pytest.mark.parametrize("bbox", BBOX) +def test_rounded_rectangle_zero_radius(bbox): # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) # Act - draw.rounded_rectangle(BBOX1, 0, fill="blue", outline="green", width=5) + draw.rounded_rectangle(bbox, 0, fill="blue", outline="green", width=5) # Assert assert_image_equal_tofile(im, "Tests/images/imagedraw_rectangle_width_fill.png") @@ -810,7 +839,9 @@ def test_rounded_rectangle_zero_radius(): "xy, suffix", [ ((20, 10, 80, 90), "x"), + ((20, 10, 81, 90), "x_odd"), ((10, 20, 90, 80), "y"), + ((10, 20, 90, 81), "y_odd"), ((20, 20, 80, 80), "both"), ], ) @@ -830,14 +861,15 @@ def test_rounded_rectangle_translucent(xy, suffix): ) -def test_floodfill(): +@pytest.mark.parametrize("bbox", BBOX) +def test_floodfill(bbox): red = ImageColor.getrgb("red") for mode, value in [("L", 1), ("RGBA", (255, 0, 0, 0)), ("RGB", red)]: # Arrange im = Image.new(mode, (W, H)) draw = ImageDraw.Draw(im) - draw.rectangle(BBOX2, outline="yellow", fill="green") + draw.rectangle(bbox, outline="yellow", fill="green") centre_point = (int(W / 2), int(H / 2)) # Act @@ -862,13 +894,14 @@ def test_floodfill(): assert_image_equal(im, Image.new("RGB", (1, 1), red)) -def test_floodfill_border(): +@pytest.mark.parametrize("bbox", BBOX) +def test_floodfill_border(bbox): # floodfill() is experimental # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) - draw.rectangle(BBOX2, outline="yellow", fill="green") + draw.rectangle(bbox, outline="yellow", fill="green") centre_point = (int(W / 2), int(H / 2)) # Act @@ -883,13 +916,14 @@ def test_floodfill_border(): assert_image_equal_tofile(im, "Tests/images/imagedraw_floodfill2.png") -def test_floodfill_thresh(): +@pytest.mark.parametrize("bbox", BBOX) +def test_floodfill_thresh(bbox): # floodfill() is experimental # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) - draw.rectangle(BBOX2, outline="darkgreen", fill="green") + draw.rectangle(bbox, outline="darkgreen", fill="green") centre_point = (int(W / 2), int(H / 2)) # Act @@ -1309,7 +1343,8 @@ def test_setting_default_font(): assert isinstance(draw.getfont(), ImageFont.ImageFont) -def test_same_color_outline(): +@pytest.mark.parametrize("bbox", BBOX) +def test_same_color_outline(bbox): # Prepare shape x0, y0 = 5, 5 x1, y1 = 5, 50 @@ -1325,12 +1360,12 @@ def test_same_color_outline(): for mode in ["RGB", "L"]: for fill, outline in [["red", None], ["red", "red"], ["red", "#f00"]]: for operation, args in { - "chord": [BBOX1, 0, 180], - "ellipse": [BBOX1], + "chord": [bbox, 0, 180], + "ellipse": [bbox], "shape": [s], - "pieslice": [BBOX1, -90, 45], + "pieslice": [bbox, -90, 45], "polygon": [[(18, 30), (85, 30), (60, 72)]], - "rectangle": [BBOX1], + "rectangle": [bbox], }.items(): # Arrange im = Image.new(mode, (W, H)) @@ -1347,20 +1382,20 @@ def test_same_color_outline(): @pytest.mark.parametrize( - "n_sides, rotation, polygon_name", - [(4, 0, "square"), (8, 0, "regular_octagon"), (4, 45, "square")], + "n_sides, polygon_name, args", + [ + (4, "square", {}), + (8, "regular_octagon", {}), + (4, "square_rotate_45", {"rotation": 45}), + (3, "triangle_width", {"width": 5, "outline": "yellow"}), + ], ) -def test_draw_regular_polygon(n_sides, rotation, polygon_name): +def test_draw_regular_polygon(n_sides, polygon_name, args): im = Image.new("RGBA", size=(W, H), color=(255, 0, 0, 0)) - filename_base = f"Tests/images/imagedraw_{polygon_name}" - filename = ( - f"{filename_base}.png" - if rotation == 0 - else f"{filename_base}_rotate_{rotation}.png" - ) + filename = f"Tests/images/imagedraw_{polygon_name}.png" draw = ImageDraw.Draw(im) bounding_circle = ((W // 2, H // 2), 25) - draw.regular_polygon(bounding_circle, n_sides, rotation=rotation, fill="red") + draw.regular_polygon(bounding_circle, n_sides, fill="red", **args) assert_image_equal_tofile(im, filename) diff --git a/Tests/test_imagedraw2.py b/Tests/test_imagedraw2.py index a8a2ee1fc..a2c2fa1f0 100644 --- a/Tests/test_imagedraw2.py +++ b/Tests/test_imagedraw2.py @@ -27,15 +27,16 @@ X1 = int(X0 * 3) Y0 = int(H / 4) Y1 = int(X0 * 3) -# Two kinds of bounding box -BBOX1 = [(X0, Y0), (X1, Y1)] -BBOX2 = [X0, Y0, X1, Y1] +# Bounding boxes +BBOX = (((X0, Y0), (X1, Y1)), [(X0, Y0), (X1, Y1)], (X0, Y0, X1, Y1), [X0, Y0, X1, Y1]) -# Two kinds of coordinate sequences -POINTS1 = [(10, 10), (20, 40), (30, 30)] -POINTS2 = [10, 10, 20, 40, 30, 30] - -KITE_POINTS = [(10, 50), (70, 10), (90, 50), (70, 90), (10, 50)] +# Coordinate sequences +POINTS = ( + ((10, 10), (20, 40), (30, 30)), + [(10, 10), (20, 40), (30, 30)], + (10, 10, 20, 40, 30, 30), + [10, 10, 20, 40, 30, 30], +) FONT_PATH = "Tests/fonts/FreeMono.ttf" @@ -52,7 +53,7 @@ def test_sanity(): draw.line(list(range(10)), pen) -@pytest.mark.parametrize("bbox", (BBOX1, BBOX2)) +@pytest.mark.parametrize("bbox", BBOX) def test_ellipse(bbox): # Arrange im = Image.new("RGB", (W, H)) @@ -80,7 +81,7 @@ def test_ellipse_edge(): assert_image_similar_tofile(im, "Tests/images/imagedraw_ellipse_edge.png", 1) -@pytest.mark.parametrize("points", (POINTS1, POINTS2)) +@pytest.mark.parametrize("points", POINTS) def test_line(points): # Arrange im = Image.new("RGB", (W, H)) @@ -94,7 +95,8 @@ def test_line(points): assert_image_equal_tofile(im, "Tests/images/imagedraw_line.png") -def test_line_pen_as_brush(): +@pytest.mark.parametrize("points", POINTS) +def test_line_pen_as_brush(points): # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw2.Draw(im) @@ -103,13 +105,13 @@ def test_line_pen_as_brush(): # Act # Pass in the pen as the brush parameter - draw.line(POINTS1, pen, brush) + draw.line(points, pen, brush) # Assert assert_image_equal_tofile(im, "Tests/images/imagedraw_line.png") -@pytest.mark.parametrize("points", (POINTS1, POINTS2)) +@pytest.mark.parametrize("points", POINTS) def test_polygon(points): # Arrange im = Image.new("RGB", (W, H)) @@ -124,7 +126,7 @@ def test_polygon(points): assert_image_equal_tofile(im, "Tests/images/imagedraw_polygon.png") -@pytest.mark.parametrize("bbox", (BBOX1, BBOX2)) +@pytest.mark.parametrize("bbox", BBOX) def test_rectangle(bbox): # Arrange im = Image.new("RGB", (W, H)) diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py index 7ea485a55..4a40d1d1d 100644 --- a/Tests/test_imagefont.py +++ b/Tests/test_imagefont.py @@ -463,6 +463,11 @@ def test_default_font(): assert_image_equal_tofile(im, "Tests/images/default_font.png") +@pytest.mark.parametrize("mode", (None, "1", "RGBA")) +def test_getbbox(font, mode): + assert (0, 4, 12, 16) == font.getbbox("A", mode) + + def test_getbbox_empty(font): # issue #2614, should not crash. assert (0, 0, 0, 0) == font.getbbox("") diff --git a/Tests/test_imagegrab.py b/Tests/test_imagegrab.py index fa88065f4..f8059eca4 100644 --- a/Tests/test_imagegrab.py +++ b/Tests/test_imagegrab.py @@ -98,3 +98,18 @@ $ms = new-object System.IO.MemoryStream(, $bytes) im = ImageGrab.grabclipboard() assert_image_equal_tofile(im, "Tests/images/hopper.png") + + @pytest.mark.skipif( + ( + sys.platform != "linux" + or not all(shutil.which(cmd) for cmd in ("wl-paste", "wl-copy")) + ), + reason="Linux with wl-clipboard only", + ) + @pytest.mark.parametrize("ext", ("gif", "png", "ico")) + def test_grabclipboard_wl_clipboard(self, ext): + image_path = "Tests/images/hopper." + ext + with open(image_path, "rb") as fp: + subprocess.call(["wl-copy"], stdin=fp) + im = ImageGrab.grabclipboard() + assert_image_equal_tofile(im, image_path) diff --git a/Tests/test_imagepath.py b/Tests/test_imagepath.py index 8f8a9f449..c112cfd87 100644 --- a/Tests/test_imagepath.py +++ b/Tests/test_imagepath.py @@ -28,7 +28,7 @@ def test_path(): (6.0, 7.0), (8.0, 9.0), ] - assert p.tolist(1) == [0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0] + assert p.tolist(True) == [0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0] assert p.getbbox() == (0.0, 1.0, 8.0, 9.0) @@ -38,48 +38,65 @@ def test_path(): p.transform((1, 0, 1, 0, 1, 1)) assert list(p) == [(1.0, 2.0), (5.0, 6.0), (9.0, 10.0)] - # alternative constructors - p = ImagePath.Path([0, 1]) - assert list(p) == [(0.0, 1.0)] - p = ImagePath.Path([0.0, 1.0]) - assert list(p) == [(0.0, 1.0)] - p = ImagePath.Path([0, 1]) - assert list(p) == [(0.0, 1.0)] - p = ImagePath.Path([(0, 1)]) - assert list(p) == [(0.0, 1.0)] - p = ImagePath.Path(p) - assert list(p) == [(0.0, 1.0)] - p = ImagePath.Path(p.tolist(0)) - assert list(p) == [(0.0, 1.0)] - p = ImagePath.Path(p.tolist(1)) - assert list(p) == [(0.0, 1.0)] - p = ImagePath.Path(array.array("f", [0, 1])) - assert list(p) == [(0.0, 1.0)] - arr = array.array("f", [0, 1]) - p = ImagePath.Path(arr.tobytes()) +@pytest.mark.parametrize( + "coords", + ( + (0, 1), + [0, 1], + (0.0, 1.0), + [0.0, 1.0], + ((0, 1),), + [(0, 1)], + ((0.0, 1.0),), + [(0.0, 1.0)], + array.array("f", [0, 1]), + array.array("f", [0, 1]).tobytes(), + ImagePath.Path((0, 1)), + ), +) +def test_path_constructors(coords): + # Arrange / Act + p = ImagePath.Path(coords) + + # Assert assert list(p) == [(0.0, 1.0)] -def test_invalid_coords(): - # Arrange - coords = ["a", "b"] - - # Act / Assert +@pytest.mark.parametrize( + "coords", + ( + ("a", "b"), + ([0, 1],), + [[0, 1]], + ([0.0, 1.0],), + [[0.0, 1.0]], + ), +) +def test_invalid_path_constructors(coords): + # Act with pytest.raises(ValueError) as e: ImagePath.Path(coords) + # Assert assert str(e.value) == "incorrect coordinate type" -def test_path_odd_number_of_coordinates(): - # Arrange - coords = [0] - - # Act / Assert +@pytest.mark.parametrize( + "coords", + ( + (0,), + [0], + (0, 1, 2), + [0, 1, 2], + ), +) +def test_path_odd_number_of_coordinates(coords): + # Act with pytest.raises(ValueError) as e: ImagePath.Path(coords) + # Assert assert str(e.value) == "wrong number of coordinates" diff --git a/Tests/test_lib_pack.py b/Tests/test_lib_pack.py index de3e7d156..f7812f62b 100644 --- a/Tests/test_lib_pack.py +++ b/Tests/test_lib_pack.py @@ -757,6 +757,7 @@ class TestLibUnpack: def test_I16(self): self.assert_unpack("I;16", "I;16", 2, 0x0201, 0x0403, 0x0605) + self.assert_unpack("I;16", "I;16B", 2, 0x0102, 0x0304, 0x0506) self.assert_unpack("I;16B", "I;16B", 2, 0x0102, 0x0304, 0x0506) self.assert_unpack("I;16L", "I;16L", 2, 0x0201, 0x0403, 0x0605) self.assert_unpack("I;16", "I;12", 2, 0x0010, 0x0203, 0x0040) diff --git a/codecov.yml b/codecov.yml index f3afccc1c..b794632fa 100644 --- a/codecov.yml +++ b/codecov.yml @@ -1,9 +1,9 @@ -# Documentation: https://docs.codecov.io/docs/codecov-yaml +# Documentation: https://docs.codecov.com/docs/codecov-yaml codecov: # Avoid "Missing base report" due to committing CHANGES.rst with "[CI skip]" # https://github.com/codecov/support/issues/363 - # https://docs.codecov.io/docs/comparing-commits + # https://docs.codecov.com/docs/comparing-commits allow_coverage_offsets: true comment: false diff --git a/depends/install_imagequant.sh b/depends/install_imagequant.sh index 362ad95a2..fd6000ee1 100755 --- a/depends/install_imagequant.sh +++ b/depends/install_imagequant.sh @@ -1,7 +1,7 @@ #!/bin/bash # install libimagequant -archive=libimagequant-4.1.1 +archive=libimagequant-4.2.0 ./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz diff --git a/docs/Guardfile b/docs/Guardfile index b689b079a..6cbf07b06 100755 --- a/docs/Guardfile +++ b/docs/Guardfile @@ -2,7 +2,7 @@ from livereload.compiler import shell from livereload.task import Task -Task.add('*.rst', shell('make html')) -Task.add('*/*.rst', shell('make html')) -Task.add('Makefile', shell('make html')) -Task.add('conf.py', shell('make html')) +Task.add("*.rst", shell("make html")) +Task.add("*/*.rst", shell("make html")) +Task.add("Makefile", shell("make html")) +Task.add("conf.py", shell("make html")) diff --git a/docs/conf.py b/docs/conf.py index 2ebcd6b2e..a2c825292 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -317,6 +317,17 @@ def setup(app): app.add_css_file("css/dark.css") +linkcheck_allowed_redirects = { + r"https://bestpractices.coreinfrastructure.org/projects/6331": r"https://bestpractices.coreinfrastructure.org/en/.*", # noqa: E501 + r"https://badges.gitter.im/python-pillow/Pillow.svg": r"https://badges.gitter.im/repo.svg", # noqa: E501 + r"https://gitter.im/python-pillow/Pillow?.*": r"https://app.gitter.im/#/room/#python-pillow_Pillow:gitter.im?.*", # noqa: E501 + r"https://pillow.readthedocs.io/?badge=latest": r"https://pillow.readthedocs.io/en/stable/?badge=latest", # noqa: E501 + r"https://pillow.readthedocs.io": r"https://pillow.readthedocs.io/en/stable/", + r"https://tidelift.com/badges/package/pypi/Pillow?.*": r"https://img.shields.io/badge/.*", # noqa: E501 + r"https://zenodo.org/badge/17549/python-pillow/Pillow.svg": r"https://zenodo.org/badge/doi/[\.0-9]+/zenodo.[0-9]+.svg", # noqa: E501 + r"https://zenodo.org/badge/latestdoi/17549/python-pillow/Pillow": r"https://zenodo.org/record/[0-9]+", # noqa: E501 +} + # sphinx.ext.extlinks # This config is a dictionary of external sites, # mapping unique short aliases to a base URL and a prefix. diff --git a/docs/deprecations.rst b/docs/deprecations.rst index 45b2f4200..62687d869 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -210,7 +210,7 @@ open-source users (and will reach EOL on 2023-12-08 for commercial licence holde Support for PyQt5 and PySide2 has been removed from ``ImageQt``. Upgrade to `PyQt6 `_ or -`PySide6 `_ instead. +`PySide6 `_ instead. Image.coerce_e ~~~~~~~~~~~~~~ diff --git a/docs/handbook/concepts.rst b/docs/handbook/concepts.rst index e40ed4687..e0975a121 100644 --- a/docs/handbook/concepts.rst +++ b/docs/handbook/concepts.rst @@ -95,9 +95,8 @@ in the upper left corner. Note that the coordinates refer to the implied pixel corners; the centre of a pixel addressed as (0, 0) actually lies at (0.5, 0.5). Coordinates are usually passed to the library as 2-tuples (x, y). Rectangles -are represented as 4-tuples, with the upper left corner given first. For -example, a rectangle covering all of an 800x600 pixel image is written as (0, -0, 800, 600). +are represented as 4-tuples, (x1, y1, x2, y2), with the upper left corner given +first. Palette ------- diff --git a/docs/installation.rst b/docs/installation.rst index a254ec8c2..ac54b037d 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -181,7 +181,7 @@ Many of Pillow's features require external libraries: * **libimagequant** provides improved color quantization - * Pillow has been tested with libimagequant **2.6-4.1.1** + * Pillow has been tested with libimagequant **2.6-4.2** * Libimagequant is licensed GPLv3, which is more restrictive than the Pillow license, therefore we will not be distributing binaries with libimagequant support enabled. @@ -448,6 +448,8 @@ These platforms are built and tested for every change. +----------------------------------+----------------------------+---------------------+ | Debian 11 Bullseye | 3.9 | x86 | +----------------------------------+----------------------------+---------------------+ +| Debian 12 Bookworm | 3.11 | x86 | ++----------------------------------+----------------------------+---------------------+ | Fedora 37 | 3.11 | x86-64 | +----------------------------------+----------------------------+---------------------+ | Fedora 38 | 3.11 | x86-64 | diff --git a/docs/reference/ImageDraw.rst b/docs/reference/ImageDraw.rst index aec7a3ef8..524f821fb 100644 --- a/docs/reference/ImageDraw.rst +++ b/docs/reference/ImageDraw.rst @@ -243,6 +243,7 @@ Methods .. py:method:: ImageDraw.line(xy, fill=None, width=0, joint=None) Draws a line between the coordinates in the ``xy`` list. + The coordinate pixels are included in the drawn line. :param xy: Sequence of either 2-tuples like ``[(x, y), (x, y), ...]`` or numeric values like ``[x, y, x, y, ...]``. @@ -287,7 +288,7 @@ Methods The polygon outline consists of straight lines between the given coordinates, plus a straight line between the last and the first - coordinate. + coordinate. The coordinate pixels are included in the drawn polygon. :param xy: Sequence of either 2-tuples like ``[(x, y), (x, y), ...]`` or numeric values like ``[x, y, x, y, ...]``. @@ -296,7 +297,7 @@ Methods :param width: The line width, in pixels. -.. py:method:: ImageDraw.regular_polygon(bounding_circle, n_sides, rotation=0, fill=None, outline=None) +.. py:method:: ImageDraw.regular_polygon(bounding_circle, n_sides, rotation=0, fill=None, outline=None, width=1) Draws a regular polygon inscribed in ``bounding_circle``, with ``n_sides``, and rotation of ``rotation`` degrees. @@ -311,6 +312,7 @@ Methods (e.g. ``rotation=90``, applies a 90 degree rotation). :param fill: Color to use for the fill. :param outline: Color to use for the outline. + :param width: The line width, in pixels. .. py:method:: ImageDraw.rectangle(xy, fill=None, outline=None, width=1) diff --git a/docs/reference/ImageGrab.rst b/docs/reference/ImageGrab.rst index 3086ba8c3..0b94032d5 100644 --- a/docs/reference/ImageGrab.rst +++ b/docs/reference/ImageGrab.rst @@ -15,8 +15,9 @@ or the clipboard to a PIL image memory. returned as an "RGBA" on macOS, or an "RGB" image otherwise. If the bounding box is omitted, the entire screen is copied. - On Linux, if ``xdisplay`` is ``None`` then ``gnome-screenshot`` will be used if it - is installed. To capture the default X11 display instead, pass ``xdisplay=""``. + On Linux, if ``xdisplay`` is ``None`` and the default X11 display does not return + a snapshot of the screen, ``gnome-screenshot`` will be used as fallback if it is + installed. To disable this behaviour, pass ``xdisplay=""`` instead. .. versionadded:: 1.1.3 (Windows), 3.0.0 (macOS), 7.1.0 (Linux) @@ -39,9 +40,11 @@ or the clipboard to a PIL image memory. .. py:function:: grabclipboard() - Take a snapshot of the clipboard image, if any. Only macOS and Windows are currently supported. + Take a snapshot of the clipboard image, if any. - .. versionadded:: 1.1.4 (Windows), 3.3.0 (macOS) + On Linux, ``wl-paste`` or ``xclip`` is required. + + .. versionadded:: 1.1.4 (Windows), 3.3.0 (macOS), 9.4.0 (Linux) :return: On Windows, an image, a list of filenames, or None if the clipboard does not contain image data or filenames. @@ -49,3 +52,5 @@ or the clipboard to a PIL image memory. On Mac, an image, or None if the clipboard does not contain image data. + + On Linux, an image. diff --git a/docs/reference/ImagePath.rst b/docs/reference/ImagePath.rst index 7c1a3ad70..500096ef7 100644 --- a/docs/reference/ImagePath.rst +++ b/docs/reference/ImagePath.rst @@ -48,7 +48,7 @@ vector data. Path objects can be passed to the methods on the Maps the path through a function. -.. py:method:: PIL.ImagePath.Path.tolist(flat=0) +.. py:method:: PIL.ImagePath.Path.tolist(flat=False) Converts the path to a Python list [(x, y), …]. diff --git a/docs/releasenotes/10.0.0.rst b/docs/releasenotes/10.0.0.rst index 3ee1a9973..3ffafafdc 100644 --- a/docs/releasenotes/10.0.0.rst +++ b/docs/releasenotes/10.0.0.rst @@ -117,7 +117,7 @@ open-source users (and will reach EOL on 2023-12-08 for commercial licence holde Support for PyQt5 and PySide2 has been removed from ``ImageQt``. Upgrade to `PyQt6 `_ or -`PySide6 `_ instead. +`PySide6 `_ instead. Image.coerce_e ^^^^^^^^^^^^^^ @@ -135,10 +135,11 @@ TODO API Changes =========== -TODO -^^^^ +Added line width parameter to ImageDraw regular_polygon +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -TODO +An optional line ``width`` parameter has been added to +``ImageDraw.Draw.regular_polygon``. API Additions ============= @@ -159,7 +160,20 @@ TODO Other Changes ============= -TODO -^^^^ +Support display_jpeg() in IPython +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -TODO +In addition to ``display()`` and ``display_png``, ``display_jpeg()`` can now +also be used to display images in IPython:: + + from PIL import Image + from IPython.display import display_jpeg + + im = Image.new("RGB", (100, 100), (255, 0, 0)) + display_jpeg(im) + +Support reading signed 8-bit TIFF images +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +TIFF images with signed integer data, 8 bits per sample and a photometric +interpretaton of BlackIsZero can now be read. diff --git a/docs/releasenotes/9.2.0.rst b/docs/releasenotes/9.2.0.rst index 8d8bfc9f8..b875edf8e 100644 --- a/docs/releasenotes/9.2.0.rst +++ b/docs/releasenotes/9.2.0.rst @@ -15,7 +15,7 @@ open-source users (and will reach EOL on 2023-12-08 for commercial licence holde Support for PyQt5 and PySide2 has been deprecated from ``ImageQt`` and will be removed in Pillow 10 (2023-07-01). Upgrade to `PyQt6 `_ or -`PySide6 `_ instead. +`PySide6 `_ instead. FreeTypeFont.getmask2 fill parameter ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/setup.py b/setup.py index 4aed148e8..0b6b02077 100755 --- a/setup.py +++ b/setup.py @@ -10,6 +10,7 @@ import os import re +import shutil import struct import subprocess import sys @@ -150,6 +151,7 @@ def _dbg(s, tp=None): def _find_library_dirs_ldconfig(): # Based on ctypes.util from Python 2 + ldconfig = "ldconfig" if shutil.which("ldconfig") else "/sbin/ldconfig" if sys.platform.startswith("linux") or sys.platform.startswith("gnu"): if struct.calcsize("l") == 4: machine = os.uname()[4] + "-32" @@ -166,14 +168,14 @@ def _find_library_dirs_ldconfig(): # Assuming GLIBC's ldconfig (with option -p) # Alpine Linux uses musl that can't print cache - args = ["ldconfig", "-p"] + args = [ldconfig, "-p"] expr = rf".*\({abi_type}.*\) => (.*)" env = dict(os.environ) env["LC_ALL"] = "C" env["LANG"] = "C" elif sys.platform.startswith("freebsd"): - args = ["ldconfig", "-r"] + args = [ldconfig, "-r"] expr = r".* => (.*)" env = {} diff --git a/src/PIL/EpsImagePlugin.py b/src/PIL/EpsImagePlugin.py index 1c88d22c7..6b1b5947e 100644 --- a/src/PIL/EpsImagePlugin.py +++ b/src/PIL/EpsImagePlugin.py @@ -134,6 +134,13 @@ def Ghostscript(tile, size, fp, scale=1, transparency=False): if gs_windows_binary is not None: if not gs_windows_binary: + try: + os.unlink(outfile) + if infile_temp: + os.unlink(infile_temp) + except OSError: + pass + msg = "Unable to locate Ghostscript on paths" raise OSError(msg) command[0] = gs_windows_binary @@ -354,7 +361,6 @@ class EpsImageFile(ImageFile.ImageFile): check_required_header_comments() if not self._size: - self._size = 1, 1 # errors if this isn't set. why (1,1)? msg = "cannot determine EPS bounding box" raise OSError(msg) diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index eadee1560..2f92e9467 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -879,7 +879,7 @@ def _get_palette_bytes(im): :param im: Image object :returns: Bytes, len<=768 suitable for inclusion in gif header """ - return im.palette.palette + return im.palette.palette if im.palette else b"" def _get_background(im, info_background): diff --git a/src/PIL/IcnsImagePlugin.py b/src/PIL/IcnsImagePlugin.py index c2f050edd..27cb89f73 100644 --- a/src/PIL/IcnsImagePlugin.py +++ b/src/PIL/IcnsImagePlugin.py @@ -22,11 +22,11 @@ import os import struct import sys -from PIL import Image, ImageFile, PngImagePlugin, features +from . import Image, ImageFile, PngImagePlugin, features enable_jpeg2k = features.check_codec("jpg_2000") if enable_jpeg2k: - from PIL import Jpeg2KImagePlugin + from . import Jpeg2KImagePlugin MAGIC = b"icns" HEADERSIZE = 8 diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 8eebd28f9..4413bae28 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -633,19 +633,34 @@ class Image: ) ) - def _repr_png_(self): - """iPython display hook support + def _repr_image(self, image_format): + """Helper function for iPython display hook. - :returns: png version of the image as bytes + :param image_format: Image format. + :returns: image as bytes, saved into the given format. """ b = io.BytesIO() try: - self.save(b, "PNG") + self.save(b, image_format) except Exception as e: - msg = "Could not save to PNG for display" + msg = f"Could not save to {image_format} for display" raise ValueError(msg) from e return b.getvalue() + def _repr_png_(self): + """iPython display hook support for PNG format. + + :returns: PNG version of the image as bytes + """ + return self._repr_image("PNG") + + def _repr_jpeg_(self): + """iPython display hook support for JPEG format. + + :returns: JPEG version of the image as bytes + """ + return self._repr_image("JPEG") + @property def __array_interface__(self): # numpy array interface support @@ -1108,7 +1123,6 @@ class Image: Available methods are :data:`Dither.NONE` or :data:`Dither.FLOYDSTEINBERG` (default). :returns: A new image - """ self.load() @@ -1240,7 +1254,7 @@ class Image: if ymargin is None: ymargin = xmargin self.load() - return self._new(self.im.expand(xmargin, ymargin, 0)) + return self._new(self.im.expand(xmargin, ymargin)) def filter(self, filter): """ diff --git a/src/PIL/ImageCms.py b/src/PIL/ImageCms.py index 38cbab19c..3a337f9f2 100644 --- a/src/PIL/ImageCms.py +++ b/src/PIL/ImageCms.py @@ -18,10 +18,10 @@ import sys from enum import IntEnum -from PIL import Image +from . import Image try: - from PIL import _imagingcms + from . import _imagingcms except ImportError as ex: # Allow error import for doc purposes, but error out when accessing # anything in core. @@ -271,7 +271,7 @@ def get_display_profile(handle=None): if sys.platform != "win32": return None - from PIL import ImageWin + from . import ImageWin if isinstance(handle, ImageWin.HDC): profile = core.get_display_profile_win32(handle, 1) diff --git a/src/PIL/ImageDraw.py b/src/PIL/ImageDraw.py index e9ccf8041..7d1790faa 100644 --- a/src/PIL/ImageDraw.py +++ b/src/PIL/ImageDraw.py @@ -279,11 +279,11 @@ class ImageDraw: self.im.paste(im.im, (0, 0) + im.size, mask.im) def regular_polygon( - self, bounding_circle, n_sides, rotation=0, fill=None, outline=None + self, bounding_circle, n_sides, rotation=0, fill=None, outline=None, width=1 ): """Draw a regular polygon.""" xy = _compute_regular_polygon_vertices(bounding_circle, n_sides, rotation) - self.polygon(xy, fill, outline) + self.polygon(xy, fill, outline, width) def rectangle(self, xy, fill=None, outline=None, width=1): """Draw a rectangle.""" @@ -314,11 +314,11 @@ class ImageDraw: full_x, full_y = False, False if all(corners): - full_x = d >= x1 - x0 + full_x = d >= x1 - x0 - 1 if full_x: # The two left and two right corners are joined d = x1 - x0 - full_y = d >= y1 - y0 + full_y = d >= y1 - y0 - 1 if full_y: # The two top and two bottom corners are joined d = y1 - y0 diff --git a/src/PIL/ImageFilter.py b/src/PIL/ImageFilter.py index 63d6dcf5c..33bc7cc2e 100644 --- a/src/PIL/ImageFilter.py +++ b/src/PIL/ImageFilter.py @@ -35,7 +35,7 @@ class BuiltinFilter(MultibandFilter): class Kernel(BuiltinFilter): """ - Create a convolution kernel. The current version only + Create a convolution kernel. The current version only supports 3x3 and 5x5 integer and floating point kernels. In the current version, kernels can only be applied to @@ -43,9 +43,10 @@ class Kernel(BuiltinFilter): :param size: Kernel size, given as (width, height). In the current version, this must be (3,3) or (5,5). - :param kernel: A sequence containing kernel weights. + :param kernel: A sequence containing kernel weights. The kernel will + be flipped vertically before being applied to the image. :param scale: Scale factor. If given, the result for each pixel is - divided by this value. The default is the sum of the + divided by this value. The default is the sum of the kernel weights. :param offset: Offset. If given, this value is added to the result, after it has been divided by the scale factor. diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index ea4549cf5..3ddc1aaad 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -26,7 +26,6 @@ # import base64 -import math import os import sys import warnings @@ -226,10 +225,6 @@ class FreeTypeFont: path, size, index, encoding, layout_engine = state self.__init__(path, size, index, encoding, layout_engine) - def _multiline_split(self, text): - split_character = "\n" if isinstance(text, str) else b"\n" - return text.split(split_character) - def getname(self): """ :return: A tuple of the font family (e.g. Helvetica) and the font style @@ -551,28 +546,23 @@ class FreeTypeFont: :py:mod:`PIL.Image.core` interface module, and the text offset, the gap between the starting coordinate and the first marking """ - size, offset = self.font.getsize( - text, mode, direction, features, language, anchor - ) if start is None: start = (0, 0) - size = tuple(math.ceil(size[i] + stroke_width * 2 + start[i]) for i in range(2)) - offset = offset[0] - stroke_width, offset[1] - stroke_width + im, size, offset = self.font.render( + text, + Image.core.fill, + mode, + direction, + features, + language, + stroke_width, + anchor, + ink, + start[0], + start[1], + Image.MAX_IMAGE_PIXELS, + ) Image._decompression_bomb_check(size) - im = Image.core.fill("RGBA" if mode == "RGBA" else "L", size, 0) - if min(size): - self.font.render( - text, - im.id, - mode, - direction, - features, - language, - stroke_width, - ink, - start[0], - start[1], - ) return im, offset def font_variant( diff --git a/src/PIL/ImageGrab.py b/src/PIL/ImageGrab.py index 982f77f20..39ecdf420 100644 --- a/src/PIL/ImageGrab.py +++ b/src/PIL/ImageGrab.py @@ -15,6 +15,7 @@ # See the README file for information on usage and redistribution. # +import io import os import shutil import subprocess @@ -61,7 +62,17 @@ def grab(bbox=None, include_layered_windows=False, all_screens=False, xdisplay=N left, top, right, bottom = bbox im = im.crop((left - x0, top - y0, right - x0, bottom - y0)) return im - elif shutil.which("gnome-screenshot"): + try: + if not Image.core.HAVE_XCB: + msg = "Pillow was built without XCB support" + raise OSError(msg) + size, data = Image.core.grabscreen_x11(xdisplay) + except OSError: + if ( + xdisplay is None + and sys.platform not in ("darwin", "win32") + and shutil.which("gnome-screenshot") + ): fh, filepath = tempfile.mkstemp(".png") os.close(fh) subprocess.call(["gnome-screenshot", "-f", filepath]) @@ -73,15 +84,13 @@ def grab(bbox=None, include_layered_windows=False, all_screens=False, xdisplay=N im.close() return im_cropped return im - # use xdisplay=None for default display on non-win32/macOS systems - if not Image.core.HAVE_XCB: - msg = "Pillow was built without XCB support" - raise OSError(msg) - size, data = Image.core.grabscreen_x11(xdisplay) - im = Image.frombytes("RGB", size, data, "raw", "BGRX", size[0] * 4, 1) - if bbox: - im = im.crop(bbox) - return im + else: + raise + else: + im = Image.frombytes("RGB", size, data, "raw", "BGRX", size[0] * 4, 1) + if bbox: + im = im.crop(bbox) + return im def grabclipboard(): @@ -120,8 +129,6 @@ def grabclipboard(): files = data[o:].decode("mbcs").split("\0") return files[: files.index("")] if isinstance(data, bytes): - import io - data = io.BytesIO(data) if fmt == "png": from . import PngImagePlugin @@ -134,16 +141,29 @@ def grabclipboard(): return None else: if shutil.which("wl-paste"): + output = subprocess.check_output(["wl-paste", "-l"]).decode() + mimetypes = output.splitlines() + if "image/png" in mimetypes: + mimetype = "image/png" + elif mimetypes: + mimetype = mimetypes[0] + else: + mimetype = None + args = ["wl-paste"] + if mimetype: + args.extend(["-t", mimetype]) elif shutil.which("xclip"): args = ["xclip", "-selection", "clipboard", "-t", "image/png", "-o"] else: msg = "wl-paste or xclip is required for ImageGrab.grabclipboard() on Linux" raise NotImplementedError(msg) - fh, filepath = tempfile.mkstemp() - subprocess.call(args, stdout=fh) - os.close(fh) - im = Image.open(filepath) + p = subprocess.run(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + err = p.stderr + if err: + msg = f"{args[0]} error: {err.strip().decode()}" + raise ChildProcessError(msg) + data = io.BytesIO(p.stdout) + im = Image.open(data) im.load() - os.unlink(filepath) return im diff --git a/src/PIL/ImageShow.py b/src/PIL/ImageShow.py index 3f68a2696..8b1c3f8bb 100644 --- a/src/PIL/ImageShow.py +++ b/src/PIL/ImageShow.py @@ -17,7 +17,7 @@ import subprocess import sys from shlex import quote -from PIL import Image +from . import Image _viewers = [] diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index 5dd1a61af..dfc7e6e9f 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -457,6 +457,11 @@ class JpegImageFile(ImageFile.ImageFile): if os.path.exists(self.filename): subprocess.check_call(["djpeg", "-outfile", path, self.filename]) else: + try: + os.unlink(path) + except OSError: + pass + msg = "Invalid Filename" raise ValueError(msg) diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py index 7afd6a2a6..ea3052fbf 100644 --- a/src/PIL/PngImagePlugin.py +++ b/src/PIL/PngImagePlugin.py @@ -1146,11 +1146,14 @@ def _write_multiple_frames(im, fp, chunk, rawmode, default_image, append_images) and prev_disposal == encoderinfo.get("disposal") and prev_blend == encoderinfo.get("blend") ): - if isinstance(duration, (list, tuple)): - previous["encoderinfo"]["duration"] += encoderinfo["duration"] + previous["encoderinfo"]["duration"] += encoderinfo.get( + "duration", duration + ) continue else: bbox = None + if "duration" not in encoderinfo: + encoderinfo["duration"] = duration im_frames.append({"im": im_frame, "bbox": bbox, "encoderinfo": encoderinfo}) # animation control @@ -1175,7 +1178,7 @@ def _write_multiple_frames(im, fp, chunk, rawmode, default_image, append_images) im_frame = im_frame.crop(bbox) size = im_frame.size encoderinfo = frame_data["encoderinfo"] - frame_duration = int(round(encoderinfo.get("duration", duration))) + frame_duration = int(round(encoderinfo["duration"])) frame_disposal = encoderinfo.get("disposal", disposal) frame_blend = encoderinfo.get("blend", blend) # frame control diff --git a/src/PIL/SpiderImagePlugin.py b/src/PIL/SpiderImagePlugin.py index eac27e679..5614957c1 100644 --- a/src/PIL/SpiderImagePlugin.py +++ b/src/PIL/SpiderImagePlugin.py @@ -36,7 +36,7 @@ import os import struct import sys -from PIL import Image, ImageFile +from . import Image, ImageFile def isInt(f): @@ -191,7 +191,7 @@ class SpiderImageFile(ImageFile.ImageFile): # returns a ImageTk.PhotoImage object, after rescaling to 0..255 def tkPhotoImage(self): - from PIL import ImageTk + from . import ImageTk return ImageTk.PhotoImage(self.convert2byte(), palette=256) diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 3d4d0910a..8f975193a 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -170,6 +170,8 @@ OPEN_INFO = { (MM, 0, (1,), 2, (8,), ()): ("L", "L;IR"), (II, 1, (1,), 1, (8,), ()): ("L", "L"), (MM, 1, (1,), 1, (8,), ()): ("L", "L"), + (II, 1, (2,), 1, (8,), ()): ("L", "L"), + (MM, 1, (2,), 1, (8,), ()): ("L", "L"), (II, 1, (1,), 2, (8,), ()): ("L", "L;R"), (MM, 1, (1,), 2, (8,), ()): ("L", "L;R"), (II, 1, (1,), 1, (12,), ()): ("I;16", "I;12"), @@ -1892,6 +1894,10 @@ class AppendingTiffWriter: 8, # srational 4, # float 8, # double + 4, # ifd + 2, # unicode + 4, # complex + 8, # long8 ] # StripOffsets = 273 diff --git a/src/_imaging.c b/src/_imaging.c index 87f5b6705..7b4174d6f 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -1027,12 +1027,11 @@ _crop(ImagingObject *self, PyObject *args) { static PyObject * _expand_image(ImagingObject *self, PyObject *args) { int x, y; - int mode = 0; - if (!PyArg_ParseTuple(args, "ii|i", &x, &y, &mode)) { + if (!PyArg_ParseTuple(args, "ii", &x, &y)) { return NULL; } - return PyImagingNew(ImagingExpand(self->image, x, y, mode)); + return PyImagingNew(ImagingExpand(self->image, x, y)); } static PyObject * diff --git a/src/_imagingft.c b/src/_imagingft.c index 78e3f7f10..25a4d3517 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -132,6 +132,27 @@ getfont(PyObject *self_, PyObject *args, PyObject *kw) { return NULL; } +#if PY_MAJOR_VERSION > 3 || PY_MINOR_VERSION > 11 + PyConfig config; + PyConfig_InitPythonConfig(&config); + if (!PyArg_ParseTupleAndKeywords( + args, + kw, + "etf|nsy#n", + kwlist, + config.filesystem_encoding, + &filename, + &size, + &index, + &encoding, + &font_bytes, + &font_bytes_size, + &layout_engine)) { + PyConfig_Clear(&config); + return NULL; + } + PyConfig_Clear(&config); +#else if (!PyArg_ParseTupleAndKeywords( args, kw, @@ -147,6 +168,7 @@ getfont(PyObject *self_, PyObject *args, PyObject *kw) { &layout_engine)) { return NULL; } +#endif self = PyObject_New(FontObject, &Font_Type); if (!self) { @@ -232,9 +254,7 @@ text_layout_raqm( const char *dir, PyObject *features, const char *lang, - GlyphInfo **glyph_info, - int mask, - int color) { + GlyphInfo **glyph_info) { size_t i = 0, count = 0, start = 0; raqm_t *rq; raqm_glyph_t *glyphs = NULL; @@ -471,7 +491,7 @@ text_layout( #ifdef HAVE_RAQM if (have_raqm && self->layout_engine == LAYOUT_RAQM) { count = text_layout_raqm( - string, self, dir, features, lang, glyph_info, mask, color); + string, self, dir, features, lang, glyph_info); } else #endif { @@ -529,73 +549,25 @@ font_getlength(FontObject *self, PyObject *args) { return PyLong_FromLong(length); } -static PyObject * -font_getsize(FontObject *self, PyObject *args) { +static int +bounding_box_and_anchors(FT_Face face, const char *anchor, int horizontal_dir, GlyphInfo *glyph_info, size_t count, int load_flags, int *width, int *height, int *x_offset, int *y_offset) { int position; /* pen position along primary axis, in 26.6 precision */ int advanced; /* pen position along primary axis, in pixels */ int px, py; /* position of current glyph, in pixels */ int x_min, x_max, y_min, y_max; /* text bounding box, in pixels */ int x_anchor, y_anchor; /* offset of point drawn at (0, 0), in pixels */ - int load_flags; /* FreeType load_flags parameter */ int error; - FT_Face face; FT_Glyph glyph; - FT_BBox bbox; /* glyph bounding box */ - GlyphInfo *glyph_info = NULL; /* computed text layout */ - size_t i, count; /* glyph_info index and length */ - int horizontal_dir; /* is primary axis horizontal? */ - int mask = 0; /* is FT_LOAD_TARGET_MONO enabled? */ - int color = 0; /* is FT_LOAD_COLOR enabled? */ - const char *mode = NULL; - const char *dir = NULL; - const char *lang = NULL; - const char *anchor = NULL; - PyObject *features = Py_None; - PyObject *string; - - /* calculate size and bearing for a given string */ - - if (!PyArg_ParseTuple( - args, "O|zzOzz:getsize", &string, &mode, &dir, &features, &lang, &anchor)) { - return NULL; - } - - horizontal_dir = dir && strcmp(dir, "ttb") == 0 ? 0 : 1; - - mask = mode && strcmp(mode, "1") == 0; - color = mode && strcmp(mode, "RGBA") == 0; - - if (anchor == NULL) { - anchor = horizontal_dir ? "la" : "lt"; - } - if (strlen(anchor) != 2) { - goto bad_anchor; - } - - count = text_layout(string, self, dir, features, lang, &glyph_info, mask, color); - if (PyErr_Occurred()) { - return NULL; - } - - load_flags = FT_LOAD_DEFAULT; - if (mask) { - load_flags |= FT_LOAD_TARGET_MONO; - } - if (color) { - load_flags |= FT_LOAD_COLOR; - } - + FT_BBox bbox; /* glyph bounding box */ + size_t i; /* glyph_info index */ /* * text bounds are given by: * - bounding boxes of individual glyphs * - pen line, i.e. 0 to `advanced` along primary axis * this means point (0, 0) is part of the text bounding box */ - face = NULL; position = x_min = x_max = y_min = y_max = 0; for (i = 0; i < count; i++) { - face = self->face; - if (horizontal_dir) { px = PIXEL(position + glyph_info[i].x_offset); py = PIXEL(glyph_info[i].y_offset); @@ -618,12 +590,14 @@ font_getsize(FontObject *self, PyObject *args) { error = FT_Load_Glyph(face, glyph_info[i].index, load_flags); if (error) { - return geterror(error); + geterror(error); + return 1; } error = FT_Get_Glyph(face->glyph, &glyph); if (error) { - return geterror(error); + geterror(error); + return 1; } FT_Glyph_Get_CBox(glyph, FT_GLYPH_BBOX_PIXELS, &bbox); @@ -647,13 +621,15 @@ font_getsize(FontObject *self, PyObject *args) { FT_Done_Glyph(glyph); } - if (glyph_info) { - PyMem_Free(glyph_info); - glyph_info = NULL; + if (anchor == NULL) { + anchor = horizontal_dir ? "la" : "lt"; + } + if (strlen(anchor) != 2) { + goto bad_anchor; } x_anchor = y_anchor = 0; - if (face) { + if (count) { if (horizontal_dir) { switch (anchor[0]) { case 'l': // left @@ -671,15 +647,15 @@ font_getsize(FontObject *self, PyObject *args) { } switch (anchor[1]) { case 'a': // ascender - y_anchor = PIXEL(self->face->size->metrics.ascender); + y_anchor = PIXEL(face->size->metrics.ascender); break; case 't': // top y_anchor = y_max; break; case 'm': // middle (ascender + descender) / 2 y_anchor = PIXEL( - (self->face->size->metrics.ascender + - self->face->size->metrics.descender) / + (face->size->metrics.ascender + + face->size->metrics.descender) / 2); break; case 's': // horizontal baseline @@ -689,7 +665,7 @@ font_getsize(FontObject *self, PyObject *args) { y_anchor = y_min; break; case 'd': // descender - y_anchor = PIXEL(self->face->size->metrics.descender); + y_anchor = PIXEL(face->size->metrics.descender); break; default: goto bad_anchor; @@ -729,17 +705,74 @@ font_getsize(FontObject *self, PyObject *args) { } } } - - return Py_BuildValue( - "(ii)(ii)", - (x_max - x_min), - (y_max - y_min), - (-x_anchor + x_min), - -(-y_anchor + y_max)); + *width = x_max - x_min; + *height = y_max - y_min; + *x_offset = -x_anchor + x_min; + *y_offset = -(-y_anchor + y_max); + return 0; bad_anchor: PyErr_Format(PyExc_ValueError, "bad anchor specified: %s", anchor); - return NULL; + return 1; +} + +static PyObject * +font_getsize(FontObject *self, PyObject *args) { + int width, height, x_offset, y_offset; + int load_flags; /* FreeType load_flags parameter */ + int error; + GlyphInfo *glyph_info = NULL; /* computed text layout */ + size_t count; /* glyph_info length */ + int horizontal_dir; /* is primary axis horizontal? */ + int mask = 0; /* is FT_LOAD_TARGET_MONO enabled? */ + int color = 0; /* is FT_LOAD_COLOR enabled? */ + const char *mode = NULL; + const char *dir = NULL; + const char *lang = NULL; + const char *anchor = NULL; + PyObject *features = Py_None; + PyObject *string; + + /* calculate size and bearing for a given string */ + + if (!PyArg_ParseTuple( + args, "O|zzOzz:getsize", &string, &mode, &dir, &features, &lang, &anchor)) { + return NULL; + } + + horizontal_dir = dir && strcmp(dir, "ttb") == 0 ? 0 : 1; + + mask = mode && strcmp(mode, "1") == 0; + color = mode && strcmp(mode, "RGBA") == 0; + + count = text_layout(string, self, dir, features, lang, &glyph_info, mask, color); + if (PyErr_Occurred()) { + return NULL; + } + + load_flags = FT_LOAD_DEFAULT; + if (mask) { + load_flags |= FT_LOAD_TARGET_MONO; + } + if (color) { + load_flags |= FT_LOAD_COLOR; + } + + error = bounding_box_and_anchors(self->face, anchor, horizontal_dir, glyph_info, count, load_flags, &width, &height, &x_offset, &y_offset); + if (glyph_info) { + PyMem_Free(glyph_info); + glyph_info = NULL; + } + if (error) { + return NULL; + } + + return Py_BuildValue( + "(ii)(ii)", + width, + height, + x_offset, + y_offset); } static PyObject * @@ -763,6 +796,7 @@ font_render(FontObject *self, PyObject *args) { unsigned int bitmap_y; /* glyph bitmap y index */ unsigned char *source; /* glyph bitmap source buffer */ unsigned char convert_scale; /* scale factor for non-8bpp bitmaps */ + PyObject *image; Imaging im; Py_ssize_t id; int mask = 0; /* is FT_LOAD_TARGET_MONO enabled? */ @@ -773,27 +807,34 @@ font_render(FontObject *self, PyObject *args) { const char *mode = NULL; const char *dir = NULL; const char *lang = NULL; + const char *anchor = NULL; PyObject *features = Py_None; PyObject *string; + PyObject *fill; float x_start = 0; float y_start = 0; + int width, height, x_offset, y_offset; + int horizontal_dir; /* is primary axis horizontal? */ + PyObject *max_image_pixels = Py_None; /* render string into given buffer (the buffer *must* have the right size, or this will crash) */ if (!PyArg_ParseTuple( args, - "On|zzOziLff:render", + "OO|zzOzizLffO:render", &string, - &id, + &fill, &mode, &dir, &features, &lang, &stroke_width, + &anchor, &foreground_ink_long, &x_start, - &y_start)) { + &y_start, + &max_image_pixels)) { return NULL; } @@ -819,8 +860,41 @@ font_render(FontObject *self, PyObject *args) { if (PyErr_Occurred()) { return NULL; } - if (count == 0) { - Py_RETURN_NONE; + + load_flags = stroke_width ? FT_LOAD_NO_BITMAP : FT_LOAD_DEFAULT; + if (mask) { + load_flags |= FT_LOAD_TARGET_MONO; + } + if (color) { + load_flags |= FT_LOAD_COLOR; + } + + horizontal_dir = dir && strcmp(dir, "ttb") == 0 ? 0 : 1; + + error = bounding_box_and_anchors(self->face, anchor, horizontal_dir, glyph_info, count, load_flags, &width, &height, &x_offset, &y_offset); + if (error) { + PyMem_Del(glyph_info); + return NULL; + } + + width += stroke_width * 2 + ceil(x_start); + height += stroke_width * 2 + ceil(y_start); + if (max_image_pixels != Py_None) { + if (width * height > PyLong_AsLong(max_image_pixels) * 2) { + PyMem_Del(glyph_info); + return Py_BuildValue("O(ii)(ii)", Py_None, width, height, 0, 0); + } + } + + image = PyObject_CallFunction(fill, "s(ii)", strcmp(mode, "RGBA") == 0 ? "RGBA" : "L", width, height); + id = PyLong_AsSsize_t(PyObject_GetAttrString(image, "id")); + im = (Imaging)id; + + x_offset -= stroke_width; + y_offset -= stroke_width; + if (count == 0 || width == 0 || height == 0) { + PyMem_Del(glyph_info); + return Py_BuildValue("O(ii)(ii)", image, width, height, x_offset, y_offset); } if (stroke_width) { @@ -837,15 +911,6 @@ font_render(FontObject *self, PyObject *args) { 0); } - im = (Imaging)id; - load_flags = stroke_width ? FT_LOAD_NO_BITMAP : FT_LOAD_DEFAULT; - if (mask) { - load_flags |= FT_LOAD_TARGET_MONO; - } - if (color) { - load_flags |= FT_LOAD_COLOR; - } - /* * calculate x_min and y_max * must match font_getsize or there may be clipping! @@ -1042,7 +1107,7 @@ font_render(FontObject *self, PyObject *args) { } FT_Stroker_Done(stroker); PyMem_Del(glyph_info); - Py_RETURN_NONE; + return Py_BuildValue("O(ii)(ii)", image, width, height, x_offset, y_offset); glyph_error: if (stroker != NULL) { diff --git a/src/display.c b/src/display.c index e8e7b62c2..754a6ae78 100644 --- a/src/display.c +++ b/src/display.c @@ -437,8 +437,14 @@ PyImaging_GrabClipboardWin32(PyObject *self, PyObject *args) { LPCSTR format_names[] = {"DIB", "DIB", "file", "png", NULL}; if (!OpenClipboard(NULL)) { - PyErr_SetString(PyExc_OSError, "failed to open clipboard"); - return NULL; + // Maybe the clipboard is temporarily in use by another process. + // Wait and try again + Sleep(500); + + if (!OpenClipboard(NULL)) { + PyErr_SetString(PyExc_OSError, "failed to open clipboard"); + return NULL; + } } // find best format as set by clipboard owner diff --git a/src/libImaging/Filter.c b/src/libImaging/Filter.c index fab3b4948..4dcd368ca 100644 --- a/src/libImaging/Filter.c +++ b/src/libImaging/Filter.c @@ -37,8 +37,19 @@ clip8(float in) { return (UINT8)in; } +static inline INT32 +clip32(float in) { + if (in <= 0.0) { + return 0; + } + if (in >= pow(2, 31) - 1) { + return pow(2, 31) - 1; + } + return (INT32)in; +} + Imaging -ImagingExpand(Imaging imIn, int xmargin, int ymargin, int mode) { +ImagingExpand(Imaging imIn, int xmargin, int ymargin) { Imaging imOut; int x, y; ImagingSectionCookie cookie; @@ -96,8 +107,8 @@ ImagingExpand(Imaging imIn, int xmargin, int ymargin, int mode) { void ImagingFilter3x3(Imaging imOut, Imaging im, const float *kernel, float offset) { #define KERNEL1x3(in0, x, kernel, d) \ - (_i2f((UINT8)in0[x - d]) * (kernel)[0] + _i2f((UINT8)in0[x]) * (kernel)[1] + \ - _i2f((UINT8)in0[x + d]) * (kernel)[2]) + (_i2f(in0[x - d]) * (kernel)[0] + _i2f(in0[x]) * (kernel)[1] + \ + _i2f(in0[x + d]) * (kernel)[2]) int x = 0, y = 0; @@ -105,21 +116,40 @@ ImagingFilter3x3(Imaging imOut, Imaging im, const float *kernel, float offset) { if (im->bands == 1) { // Add one time for rounding offset += 0.5; - for (y = 1; y < im->ysize - 1; y++) { - UINT8 *in_1 = (UINT8 *)im->image[y - 1]; - UINT8 *in0 = (UINT8 *)im->image[y]; - UINT8 *in1 = (UINT8 *)im->image[y + 1]; - UINT8 *out = (UINT8 *)imOut->image[y]; + if (im->type == IMAGING_TYPE_INT32) { + for (y = 1; y < im->ysize - 1; y++) { + INT32 *in_1 = (INT32 *)im->image[y - 1]; + INT32 *in0 = (INT32 *)im->image[y]; + INT32 *in1 = (INT32 *)im->image[y + 1]; + INT32 *out = (INT32 *)imOut->image[y]; - out[0] = in0[0]; - for (x = 1; x < im->xsize - 1; x++) { - float ss = offset; - ss += KERNEL1x3(in1, x, &kernel[0], 1); - ss += KERNEL1x3(in0, x, &kernel[3], 1); - ss += KERNEL1x3(in_1, x, &kernel[6], 1); - out[x] = clip8(ss); + out[0] = in0[0]; + for (x = 1; x < im->xsize - 1; x++) { + float ss = offset; + ss += KERNEL1x3(in1, x, &kernel[0], 1); + ss += KERNEL1x3(in0, x, &kernel[3], 1); + ss += KERNEL1x3(in_1, x, &kernel[6], 1); + out[x] = clip32(ss); + } + out[x] = in0[x]; + } + } else { + for (y = 1; y < im->ysize - 1; y++) { + UINT8 *in_1 = (UINT8 *)im->image[y - 1]; + UINT8 *in0 = (UINT8 *)im->image[y]; + UINT8 *in1 = (UINT8 *)im->image[y + 1]; + UINT8 *out = (UINT8 *)imOut->image[y]; + + out[0] = in0[0]; + for (x = 1; x < im->xsize - 1; x++) { + float ss = offset; + ss += KERNEL1x3(in1, x, &kernel[0], 1); + ss += KERNEL1x3(in0, x, &kernel[3], 1); + ss += KERNEL1x3(in_1, x, &kernel[6], 1); + out[x] = clip8(ss); + } + out[x] = in0[x]; } - out[x] = in0[x]; } } else { // Add one time for rounding @@ -195,10 +225,10 @@ ImagingFilter3x3(Imaging imOut, Imaging im, const float *kernel, float offset) { void ImagingFilter5x5(Imaging imOut, Imaging im, const float *kernel, float offset) { #define KERNEL1x5(in0, x, kernel, d) \ - (_i2f((UINT8)in0[x - d - d]) * (kernel)[0] + \ - _i2f((UINT8)in0[x - d]) * (kernel)[1] + _i2f((UINT8)in0[x]) * (kernel)[2] + \ - _i2f((UINT8)in0[x + d]) * (kernel)[3] + \ - _i2f((UINT8)in0[x + d + d]) * (kernel)[4]) + (_i2f(in0[x - d - d]) * (kernel)[0] + \ + _i2f(in0[x - d]) * (kernel)[1] + _i2f(in0[x]) * (kernel)[2] + \ + _i2f(in0[x + d]) * (kernel)[3] + \ + _i2f(in0[x + d + d]) * (kernel)[4]) int x = 0, y = 0; @@ -207,27 +237,52 @@ ImagingFilter5x5(Imaging imOut, Imaging im, const float *kernel, float offset) { if (im->bands == 1) { // Add one time for rounding offset += 0.5; - for (y = 2; y < im->ysize - 2; y++) { - UINT8 *in_2 = (UINT8 *)im->image[y - 2]; - UINT8 *in_1 = (UINT8 *)im->image[y - 1]; - UINT8 *in0 = (UINT8 *)im->image[y]; - UINT8 *in1 = (UINT8 *)im->image[y + 1]; - UINT8 *in2 = (UINT8 *)im->image[y + 2]; - UINT8 *out = (UINT8 *)imOut->image[y]; + if (im->type == IMAGING_TYPE_INT32) { + for (y = 2; y < im->ysize - 2; y++) { + INT32 *in_2 = (INT32 *)im->image[y - 2]; + INT32 *in_1 = (INT32 *)im->image[y - 1]; + INT32 *in0 = (INT32 *)im->image[y]; + INT32 *in1 = (INT32 *)im->image[y + 1]; + INT32 *in2 = (INT32 *)im->image[y + 2]; + INT32 *out = (INT32 *)imOut->image[y]; - out[0] = in0[0]; - out[1] = in0[1]; - for (x = 2; x < im->xsize - 2; x++) { - float ss = offset; - ss += KERNEL1x5(in2, x, &kernel[0], 1); - ss += KERNEL1x5(in1, x, &kernel[5], 1); - ss += KERNEL1x5(in0, x, &kernel[10], 1); - ss += KERNEL1x5(in_1, x, &kernel[15], 1); - ss += KERNEL1x5(in_2, x, &kernel[20], 1); - out[x] = clip8(ss); + out[0] = in0[0]; + out[1] = in0[1]; + for (x = 2; x < im->xsize - 2; x++) { + float ss = offset; + ss += KERNEL1x5(in2, x, &kernel[0], 1); + ss += KERNEL1x5(in1, x, &kernel[5], 1); + ss += KERNEL1x5(in0, x, &kernel[10], 1); + ss += KERNEL1x5(in_1, x, &kernel[15], 1); + ss += KERNEL1x5(in_2, x, &kernel[20], 1); + out[x] = clip32(ss); + } + out[x + 0] = in0[x + 0]; + out[x + 1] = in0[x + 1]; + } + } else { + for (y = 2; y < im->ysize - 2; y++) { + UINT8 *in_2 = (UINT8 *)im->image[y - 2]; + UINT8 *in_1 = (UINT8 *)im->image[y - 1]; + UINT8 *in0 = (UINT8 *)im->image[y]; + UINT8 *in1 = (UINT8 *)im->image[y + 1]; + UINT8 *in2 = (UINT8 *)im->image[y + 2]; + UINT8 *out = (UINT8 *)imOut->image[y]; + + out[0] = in0[0]; + out[1] = in0[1]; + for (x = 2; x < im->xsize - 2; x++) { + float ss = offset; + ss += KERNEL1x5(in2, x, &kernel[0], 1); + ss += KERNEL1x5(in1, x, &kernel[5], 1); + ss += KERNEL1x5(in0, x, &kernel[10], 1); + ss += KERNEL1x5(in_1, x, &kernel[15], 1); + ss += KERNEL1x5(in_2, x, &kernel[20], 1); + out[x] = clip8(ss); + } + out[x + 0] = in0[x + 0]; + out[x + 1] = in0[x + 1]; } - out[x + 0] = in0[x + 0]; - out[x + 1] = in0[x + 1]; } } else { // Add one time for rounding @@ -327,7 +382,7 @@ ImagingFilter(Imaging im, int xsize, int ysize, const FLOAT32 *kernel, FLOAT32 o Imaging imOut; ImagingSectionCookie cookie; - if (!im || im->type != IMAGING_TYPE_UINT8) { + if (im->type != IMAGING_TYPE_UINT8 && im->type != IMAGING_TYPE_INT32) { return (Imaging)ImagingError_ModeError(); } diff --git a/src/libImaging/ImPlatform.h b/src/libImaging/ImPlatform.h index af9996ca9..94781f9ec 100644 --- a/src/libImaging/ImPlatform.h +++ b/src/libImaging/ImPlatform.h @@ -58,12 +58,6 @@ #error Cannot find required 32-bit integer type #endif -#if SIZEOF_LONG == 8 -#define INT64 long -#elif SIZEOF_LONG_LONG == 8 -#define INT64 long -#endif - #define INT8 signed char #define UINT8 unsigned char diff --git a/src/libImaging/Imaging.h b/src/libImaging/Imaging.h index 42420887d..01f40ee7b 100644 --- a/src/libImaging/Imaging.h +++ b/src/libImaging/Imaging.h @@ -290,7 +290,7 @@ ImagingConvertTransparent(Imaging im, const char *mode, int r, int g, int b); extern Imaging ImagingCrop(Imaging im, int x0, int y0, int x1, int y1); extern Imaging -ImagingExpand(Imaging im, int x, int y, int mode); +ImagingExpand(Imaging im, int x, int y); extern Imaging ImagingFill(Imaging im, const void *ink); extern int diff --git a/src/libImaging/Jpeg2KEncode.c b/src/libImaging/Jpeg2KEncode.c index 0d7e896b7..de8586706 100644 --- a/src/libImaging/Jpeg2KEncode.c +++ b/src/libImaging/Jpeg2KEncode.c @@ -464,7 +464,7 @@ j2k_encode_entry(Imaging im, ImagingCodecState state) { } if (!context->num_resolutions) { - while (tile_width < (1 << (params.numresolution - 1U)) || tile_height < (1 << (params.numresolution - 1U))) { + while (tile_width < (1U << (params.numresolution - 1U)) || tile_height < (1U << (params.numresolution - 1U))) { params.numresolution -= 1; } } diff --git a/src/libImaging/Storage.c b/src/libImaging/Storage.c index 7cf00ef35..128595f65 100644 --- a/src/libImaging/Storage.c +++ b/src/libImaging/Storage.c @@ -37,8 +37,6 @@ #include "Imaging.h" #include -int ImagingNewCount = 0; - /* -------------------------------------------------------------------- * Standard image object. */ diff --git a/src/libImaging/Unpack.c b/src/libImaging/Unpack.c index 7eeadf944..206403ba6 100644 --- a/src/libImaging/Unpack.c +++ b/src/libImaging/Unpack.c @@ -1149,6 +1149,16 @@ unpackI16N_I16(UINT8 *out, const UINT8 *in, int pixels) { } } static void +unpackI16B_I16(UINT8 *out, const UINT8 *in, int pixels) { + int i; + for (i = 0; i < pixels; i++) { + out[0] = in[1]; + out[1] = in[0]; + in += 2; + out += 2; + } +} +static void unpackI16R_I16(UINT8 *out, const UINT8 *in, int pixels) { int i; for (i = 0; i < pixels; i++) { @@ -1542,10 +1552,12 @@ static struct { {"P", "P;4L", 4, unpackP4L}, {"P", "P", 8, copy1}, {"P", "P;R", 8, unpackLR}, + {"P", "L", 8, copy1}, /* palette w. alpha */ {"PA", "PA", 16, unpackLA}, {"PA", "PA;L", 16, unpackLAL}, + {"PA", "LA", 16, unpackLA}, /* true colour */ {"RGB", "RGB", 24, ImagingUnpackRGB}, @@ -1764,6 +1776,7 @@ static struct { {"I;16L", "I;16L", 16, copy2}, {"I;16N", "I;16N", 16, copy2}, + {"I;16", "I;16B", 16, unpackI16B_I16}, {"I;16", "I;16N", 16, unpackI16N_I16}, // LibTiff native->image endian. {"I;16L", "I;16N", 16, unpackI16N_I16}, // LibTiff native->image endian. {"I;16B", "I;16N", 16, unpackI16N_I16B}, diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 9b5fc5d18..89b2daad0 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -152,9 +152,9 @@ deps = { "libs": [r"*.lib"], }, "xz": { - "url": SF_PROJECTS + "/lzmautils/files/xz-5.4.2.tar.gz/download", - "filename": "xz-5.4.2.tar.gz", - "dir": "xz-5.4.2", + "url": SF_PROJECTS + "/lzmautils/files/xz-5.4.3.tar.gz/download", + "filename": "xz-5.4.3.tar.gz", + "dir": "xz-5.4.3", "license": "COPYING", "build": [ *cmds_cmake("liblzma", "-DBUILD_SHARED_LIBS:BOOL=OFF"), @@ -337,9 +337,9 @@ deps = { "libs": [r"imagequant.lib"], }, "harfbuzz": { - "url": "https://github.com/harfbuzz/harfbuzz/archive/7.2.0.zip", - "filename": "harfbuzz-7.2.0.zip", - "dir": "harfbuzz-7.2.0", + "url": "https://github.com/harfbuzz/harfbuzz/archive/7.3.0.zip", + "filename": "harfbuzz-7.3.0.zip", + "dir": "harfbuzz-7.3.0", "license": "COPYING", "build": [ *cmds_cmake( @@ -352,12 +352,12 @@ deps = { "libs": [r"*.lib"], }, "fribidi": { - "url": "https://github.com/fribidi/fribidi/archive/v1.0.12.zip", - "filename": "fribidi-1.0.12.zip", - "dir": "fribidi-1.0.12", + "url": "https://github.com/fribidi/fribidi/archive/v1.0.13.zip", + "filename": "fribidi-1.0.13.zip", + "dir": "fribidi-1.0.13", "license": "COPYING", "build": [ - cmd_copy(r"COPYING", r"{bin_dir}\fribidi-1.0.12-COPYING"), + cmd_copy(r"COPYING", r"{bin_dir}\fribidi-1.0.13-COPYING"), cmd_copy(r"{winbuild_dir}\fribidi.cmake", r"CMakeLists.txt"), *cmds_cmake("fribidi"), ],