diff --git a/.github/workflows/test-cygwin.yml b/.github/workflows/test-cygwin.yml index e7ab6466e..2ba5e741e 100644 --- a/.github/workflows/test-cygwin.yml +++ b/.github/workflows/test-cygwin.yml @@ -76,17 +76,23 @@ jobs: with: dirs: 'C:\cygwin\bin;C:\cygwin\lib\lapack' + - name: Select Python version + run: | + ln -sf c:/cygwin/bin/python3.${{ matrix.python-minor-version }} c:/cygwin/bin/python3 + + - name: Get latest NumPy version + id: latest-numpy + shell: bash.exe -eo pipefail -o igncr "{0}" + run: | + python3 -m pip list --outdated | grep numpy | sed -r 's/ +/ /g' | cut -d ' ' -f 3 | sed 's/^/version=/' >> $GITHUB_OUTPUT + - name: pip cache uses: actions/cache@v3 with: path: 'C:\cygwin\home\runneradmin\.cache\pip' - key: ${{ runner.os }}-cygwin-pip3.${{ matrix.python-minor-version }}-${{ hashFiles('.ci/install.sh') }} + key: ${{ runner.os }}-cygwin-pip3.${{ matrix.python-minor-version }}-numpy${{ steps.latest-numpy.outputs.version }}-${{ hashFiles('.ci/install.sh') }} restore-keys: | - ${{ runner.os }}-cygwin-pip3.${{ matrix.python-minor-version }}- - - - name: Select Python version - run: | - ln -sf c:/cygwin/bin/python3.${{ matrix.python-minor-version }} c:/cygwin/bin/python3 + ${{ runner.os }}-cygwin-pip3.${{ matrix.python-minor-version }}-numpy${{ steps.latest-numpy.outputs.version }}- - name: Build system information run: | @@ -96,7 +102,7 @@ jobs: run: | bash.exe .ci/install.sh - - name: Install a different NumPy + - name: Install latest NumPy shell: dash.exe -l "{0}" run: | python3 -m pip install -U numpy diff --git a/CHANGES.rst b/CHANGES.rst index 94cd6e7bc..961ce563c 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -2,6 +2,18 @@ Changelog (Pillow) ================== +10.1.0 (unreleased) +------------------- + +- Silence exceptions in _repr_jpeg_ and _repr_png_ #7266 + [mtreinish, radarhere] + +- Do not use transparency when saving GIF if it has been removed when normalizing mode #7284 + [radarhere] + +- Fix missing symbols when libtiff depends on libjpeg #7270 + [heitbaum] + 10.0.0 (2023-07-01) ------------------- diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index f4a17264f..b1c9f731f 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -1086,6 +1086,21 @@ def test_transparent_optimize(tmp_path): assert reloaded.info["transparency"] == reloaded.getpixel((252, 0)) +def test_removed_transparency(tmp_path): + out = str(tmp_path / "temp.gif") + im = Image.new("RGB", (256, 1)) + + for x in range(256): + im.putpixel((x, 0), (x, 0, 0)) + + im.info["transparency"] = (255, 255, 255) + with pytest.warns(UserWarning): + im.save(out) + + with Image.open(out) as reloaded: + assert "transparency" not in reloaded.info + + def test_rgb_transparency(tmp_path): out = str(tmp_path / "temp.gif") diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index d7dd0d298..4fb332243 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -929,7 +929,7 @@ class TestFileJpeg: assert repr_jpeg.format == "JPEG" assert_image_similar(im, repr_jpeg, 17) - def test_repr_jpeg_error(self): + def test_repr_jpeg_error_returns_none(self): im = hopper("BGR;24") assert im._repr_jpeg_() is None diff --git a/Tests/test_file_jpeg2k.py b/Tests/test_file_jpeg2k.py index b6e8215f7..99df26fc9 100644 --- a/Tests/test_file_jpeg2k.py +++ b/Tests/test_file_jpeg2k.py @@ -274,17 +274,15 @@ def test_sgnd(tmp_path): assert reloaded_signed.getpixel((0, 0)) == 128 -def test_rgba(): +@pytest.mark.parametrize("ext", (".j2k", ".jp2")) +def test_rgba(ext): # Arrange - with Image.open("Tests/images/rgb_trns_ycbc.j2k") as j2k: - with Image.open("Tests/images/rgb_trns_ycbc.jp2") as jp2: - # Act - j2k.load() - jp2.load() + with Image.open("Tests/images/rgb_trns_ycbc" + ext) as im: + # Act + im.load() - # Assert - assert j2k.mode == "RGBA" - assert jp2.mode == "RGBA" + # Assert + assert im.mode == "RGBA" @pytest.mark.parametrize("ext", (".j2k", ".jp2")) diff --git a/Tests/test_file_png.py b/Tests/test_file_png.py index 4f456f79b..21e3274c0 100644 --- a/Tests/test_file_png.py +++ b/Tests/test_file_png.py @@ -532,7 +532,7 @@ class TestFilePng: assert repr_png.format == "PNG" assert_image_equal(im, repr_png) - def test_repr_png_error(self): + def test_repr_png_error_returns_none(self): im = hopper("BGR;24") assert im._repr_png_() is None diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index fbaca5ae7..bd4995fc0 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -861,6 +861,10 @@ PPM Pillow reads and writes PBM, PGM, PPM and PNM files containing ``1``, ``L``, ``I`` or ``RGB`` data. +"Raw" (P4 to P6) formats can be read, and are used when writing. + +Since Pillow 9.2.0, "plain" (P1 to P3) formats can be read as well. + SGI ^^^ diff --git a/docs/newer-versions.csv b/docs/newer-versions.csv index d53947ff5..1457d59de 100644 --- a/docs/newer-versions.csv +++ b/docs/newer-versions.csv @@ -1,7 +1,8 @@ -Python,3.11,3.10,3.9,3.8,3.7,3.6,3.5 -Pillow >= 10,Yes,Yes,Yes,Yes,,, -Pillow 9.3 - 9.5,Yes,Yes,Yes,Yes,Yes,, -Pillow 9.0 - 9.2,,Yes,Yes,Yes,Yes,, -Pillow 8.3.2 - 8.4,,Yes,Yes,Yes,Yes,Yes, -Pillow 8.0 - 8.3.1,,,Yes,Yes,Yes,Yes, -Pillow 7.0 - 7.2,,,,Yes,Yes,Yes,Yes +Python,3.12,3.11,3.10,3.9,3.8,3.7,3.6,3.5 +Pillow >= 10.1,Yes,Yes,Yes,Yes,Yes,,, +Pillow 10.0,,Yes,Yes,Yes,Yes,,, +Pillow 9.3 - 9.5,,Yes,Yes,Yes,Yes,Yes,, +Pillow 9.0 - 9.2,,,Yes,Yes,Yes,Yes,, +Pillow 8.3.2 - 8.4,,,Yes,Yes,Yes,Yes,Yes, +Pillow 8.0 - 8.3.1,,,,Yes,Yes,Yes,Yes, +Pillow 7.0 - 7.2,,,,,Yes,Yes,Yes,Yes \ No newline at end of file diff --git a/docs/reference/ImageDraw.rst b/docs/reference/ImageDraw.rst index 31f63695e..95a40007b 100644 --- a/docs/reference/ImageDraw.rst +++ b/docs/reference/ImageDraw.rst @@ -538,7 +538,7 @@ Methods It should be a `BCP 47 language code`_. Requires libraqm. :param embedded_color: Whether to use font embedded color glyphs (COLR, CBDT, SBIX). - :return: Width for horizontal, height for vertical text. + :return: Either width for horizontal text, or height for vertical text. .. py:method:: ImageDraw.textbbox(xy, text, font=None, anchor=None, spacing=4, align="left", direction=None, features=None, language=None, stroke_width=0, embedded_color=False) diff --git a/setup.cfg b/setup.cfg index 06e95d7cc..e560f9516 100644 --- a/setup.cfg +++ b/setup.cfg @@ -16,6 +16,7 @@ classifiers = Programming Language :: Python :: 3.9 Programming Language :: Python :: 3.10 Programming Language :: Python :: 3.11 + Programming Language :: Python :: 3.12 Programming Language :: Python :: Implementation :: CPython Programming Language :: Python :: Implementation :: PyPy Topic :: Multimedia :: Graphics diff --git a/setup.py b/setup.py index 024634ad8..935166716 100755 --- a/setup.py +++ b/setup.py @@ -39,7 +39,7 @@ TIFF_ROOT = None ZLIB_ROOT = None FUZZING_BUILD = "LIB_FUZZING_ENGINE" in os.environ -if sys.platform == "win32" and sys.version_info >= (3, 12): +if sys.platform == "win32" and sys.version_info >= (3, 13): import atexit atexit.register( @@ -816,6 +816,15 @@ class pil_build_ext(build_ext): libs = self.add_imaging_libs.split() defs = [] + if feature.tiff: + libs.append(feature.tiff) + defs.append(("HAVE_LIBTIFF", None)) + if sys.platform == "win32": + # This define needs to be defined if-and-only-if it was defined + # when compiling LibTIFF. LibTIFF doesn't expose it in `tiffconf.h`, + # so we have to guess; by default it is defined in all Windows builds. + # See #4237, #5243, #5359 for more information. + defs.append(("USE_WIN32_FILEIO", None)) if feature.jpeg: libs.append(feature.jpeg) defs.append(("HAVE_LIBJPEG", None)) @@ -830,15 +839,6 @@ class pil_build_ext(build_ext): if feature.imagequant: libs.append(feature.imagequant) defs.append(("HAVE_LIBIMAGEQUANT", None)) - if feature.tiff: - libs.append(feature.tiff) - defs.append(("HAVE_LIBTIFF", None)) - if sys.platform == "win32": - # This define needs to be defined if-and-only-if it was defined - # when compiling LibTIFF. LibTIFF doesn't expose it in `tiffconf.h`, - # so we have to guess; by default it is defined in all Windows builds. - # See #4237, #5243, #5359 for more information. - defs.append(("USE_WIN32_FILEIO", None)) if feature.xcb: libs.append(feature.xcb) defs.append(("HAVE_XCB", None)) diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index cf2993e38..255643de6 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -683,11 +683,7 @@ def get_interlace(im): def _write_local_header(fp, im, offset, flags): transparent_color_exists = False try: - if "transparency" in im.encoderinfo: - transparency = im.encoderinfo["transparency"] - else: - transparency = im.info["transparency"] - transparency = int(transparency) + transparency = int(im.encoderinfo["transparency"]) except (KeyError, ValueError): pass else: diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index 05828a72f..b77dc6ab1 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -318,7 +318,7 @@ class FreeTypeFont: `_ Requires libraqm. - :return: Width for horizontal, height for vertical text. + :return: Either width for horizontal text, or height for vertical text. """ _string_length_check(text) return self.font.getlength(text, mode, direction, features, language) / 64 diff --git a/src/libImaging/Access.c b/src/libImaging/Access.c index f00939da0..dd0418696 100644 --- a/src/libImaging/Access.c +++ b/src/libImaging/Access.c @@ -46,22 +46,11 @@ add_item(const char *mode) { /* fetch individual pixel */ static void -get_pixel(Imaging im, int x, int y, void *color) { +get_pixel_32_2bands(Imaging im, int x, int y, void *color) { char *out = color; - - /* generic pixel access*/ - - if (im->image8) { - out[0] = im->image8[y][x]; - } else { - UINT8 *p = (UINT8 *)&im->image32[y][x]; - if (im->type == IMAGING_TYPE_UINT8 && im->bands == 2) { - out[0] = p[0]; - out[1] = p[3]; - return; - } - memcpy(out, p, im->pixelsize); - } + UINT8 *p = (UINT8 *)&im->image32[y][x]; + out[0] = p[0]; + out[1] = p[3]; } static void @@ -127,15 +116,6 @@ get_pixel_32B(Imaging im, int x, int y, void *color) { /* store individual pixel */ -static void -put_pixel(Imaging im, int x, int y, const void *color) { - if (im->image8) { - im->image8[y][x] = *((UINT8 *)color); - } else { - memcpy(&im->image32[y][x], color, sizeof(INT32)); - } -} - static void put_pixel_8(Imaging im, int x, int y, const void *color) { im->image8[y][x] = *((UINT8 *)color); @@ -186,8 +166,8 @@ ImagingAccessInit() { /* populate access table */ ADD("1", get_pixel_8, put_pixel_8); ADD("L", get_pixel_8, put_pixel_8); - ADD("LA", get_pixel, put_pixel); - ADD("La", get_pixel, put_pixel); + ADD("LA", get_pixel_32_2bands, put_pixel_32); + ADD("La", get_pixel_32_2bands, put_pixel_32); ADD("I", get_pixel_32, put_pixel_32); ADD("I;16", get_pixel_16L, put_pixel_16L); ADD("I;16L", get_pixel_16L, put_pixel_16L); @@ -197,7 +177,7 @@ ImagingAccessInit() { ADD("I;32B", get_pixel_32B, put_pixel_32B); ADD("F", get_pixel_32, put_pixel_32); ADD("P", get_pixel_8, put_pixel_8); - ADD("PA", get_pixel, put_pixel); + ADD("PA", get_pixel_32_2bands, put_pixel_32); ADD("RGB", get_pixel_32, put_pixel_32); ADD("RGBA", get_pixel_32, put_pixel_32); ADD("RGBa", get_pixel_32, put_pixel_32); diff --git a/tox.ini b/tox.ini index a79089f51..5388ed243 100644 --- a/tox.ini +++ b/tox.ini @@ -3,7 +3,7 @@ requires = tox>=4.2 env_list = lint - py{py3, 311, 310, 39, 38} + py{py3, 312, 311, 310, 39, 38} [testenv] deps = diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 0b7e5c824..df834a387 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -335,9 +335,9 @@ deps = { "libs": [r"imagequant.lib"], }, "harfbuzz": { - "url": "https://github.com/harfbuzz/harfbuzz/archive/7.3.0.zip", - "filename": "harfbuzz-7.3.0.zip", - "dir": "harfbuzz-7.3.0", + "url": "https://github.com/harfbuzz/harfbuzz/archive/8.0.0.zip", + "filename": "harfbuzz-8.0.0.zip", + "dir": "harfbuzz-8.0.0", "license": "COPYING", "build": [ *cmds_cmake(