diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 97f70ed84..c25e1a27d 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -16,7 +16,11 @@ ARCHIVE_SDIR=pillow-depends-main # Package versions for fresh source builds FREETYPE_VERSION=2.13.2 -HARFBUZZ_VERSION=8.5.0 +if [[ "$MB_ML_VER" != 2014 ]]; then + HARFBUZZ_VERSION=9.0.0 +else + HARFBUZZ_VERSION=8.5.0 +fi LIBPNG_VERSION=1.6.43 JPEGTURBO_VERSION=3.0.3 OPENJPEG_VERSION=2.5.2 @@ -40,7 +44,7 @@ BROTLI_VERSION=1.1.0 if [[ -n "$IS_MACOS" ]] && [[ "$CIBW_ARCHS" == "x86_64" ]]; then function build_openjpeg { - local out_dir=$(fetch_unpack https://github.com/uclouvain/openjpeg/archive/v${OPENJPEG_VERSION}.tar.gz openjpeg-${OPENJPEG_VERSION}.tar.gz) + local out_dir=$(fetch_unpack https://github.com/uclouvain/openjpeg/archive/v$OPENJPEG_VERSION.tar.gz openjpeg-$OPENJPEG_VERSION.tar.gz) (cd $out_dir \ && cmake -DCMAKE_INSTALL_PREFIX=$BUILD_PREFIX -DCMAKE_INSTALL_NAME_DIR=$BUILD_PREFIX/lib . \ && make install) @@ -50,7 +54,7 @@ fi function build_brotli { local cmake=$(get_modern_cmake) - local out_dir=$(fetch_unpack https://github.com/google/brotli/archive/v$BROTLI_VERSION.tar.gz brotli-1.1.0.tar.gz) + local out_dir=$(fetch_unpack https://github.com/google/brotli/archive/v$BROTLI_VERSION.tar.gz brotli-$BROTLI_VERSION.tar.gz) (cd $out_dir \ && $cmake -DCMAKE_INSTALL_PREFIX=$BUILD_PREFIX -DCMAKE_INSTALL_NAME_DIR=$BUILD_PREFIX/lib . \ && make install) @@ -60,6 +64,25 @@ function build_brotli { fi } +function build_harfbuzz { + if [[ "$HARFBUZZ_VERSION" == 8.5.0 ]]; then + export FREETYPE_LIBS=-lfreetype + export FREETYPE_CFLAGS=-I/usr/local/include/freetype2/ + build_simple harfbuzz $HARFBUZZ_VERSION https://github.com/harfbuzz/harfbuzz/releases/download/$HARFBUZZ_VERSION tar.xz --with-freetype=yes --with-glib=no + export FREETYPE_LIBS="" + export FREETYPE_CFLAGS="" + else + local out_dir=$(fetch_unpack https://github.com/harfbuzz/harfbuzz/releases/download/$HARFBUZZ_VERSION/$HARFBUZZ_VERSION.tar.xz harfbuzz-$HARFBUZZ_VERSION.tar.xz) + (cd $out_dir \ + && meson setup build --buildtype=release -Dfreetype=enabled -Dglib=disabled) + (cd $out_dir/build \ + && meson install) + if [[ "$MB_ML_LIBC" == "manylinux" ]]; then + cp /usr/local/lib64/libharfbuzz* /usr/local/lib + fi + fi +} + function build { if [[ -n "$IS_MACOS" ]] && [[ "$CIBW_ARCHS" == "arm64" ]]; then sudo chown -R runner /usr/local @@ -109,15 +132,7 @@ function build { build_freetype fi - if [ -z "$IS_MACOS" ]; then - export FREETYPE_LIBS=-lfreetype - export FREETYPE_CFLAGS=-I/usr/local/include/freetype2/ - fi - build_simple harfbuzz $HARFBUZZ_VERSION https://github.com/harfbuzz/harfbuzz/releases/download/$HARFBUZZ_VERSION tar.xz --with-freetype=yes --with-glib=no - if [ -z "$IS_MACOS" ]; then - export FREETYPE_LIBS="" - export FREETYPE_CFLAGS="" - fi + build_harfbuzz } # Any stuff that you need to do before you start building the wheels @@ -140,7 +155,13 @@ if [[ -n "$IS_MACOS" ]]; then brew remove --ignore-dependencies webp fi - brew install pkg-config + brew install meson pkg-config +elif [[ "$MB_ML_LIBC" == "manylinux" ]]; then + if [[ "$HARFBUZZ_VERSION" != 8.5.0 ]]; then + yum install -y meson + fi +else + apk add meson fi wrap_wheel_builder build diff --git a/CHANGES.rst b/CHANGES.rst index 15da95e09..e217c9b82 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 11.0.0 (unreleased) ------------------- +- Support converting more modes to LAB by converting to RGBA first #8358 + [radarhere] + - Deprecate support for FreeType 2.9.0 #8356 [hugovk, radarhere] diff --git a/Tests/test_image.py b/Tests/test_image.py index 97e97acaa..cf5b5a710 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -1117,6 +1117,10 @@ class TestImage: assert len(caplog.records) == 0 assert im.fp is None + def test_deprecation(self) -> None: + with pytest.warns(DeprecationWarning): + assert not Image.isImageType(None) + class TestImageBytes: @pytest.mark.parametrize("mode", Image.MODES + ["BGR;15", "BGR;16", "BGR;24"]) diff --git a/Tests/test_imagecms.py b/Tests/test_imagecms.py index 5ee5fcedf..f062651f0 100644 --- a/Tests/test_imagecms.py +++ b/Tests/test_imagecms.py @@ -696,6 +696,12 @@ def test_rgb_lab(mode: str) -> None: assert value[:3] == (0, 255, 255) +def test_cmyk_lab() -> None: + im = Image.new("CMYK", (1, 1)) + converted_im = im.convert("LAB") + assert converted_im.getpixel((0, 0)) == (255, 128, 128) + + def test_deprecation() -> None: with pytest.warns(DeprecationWarning): assert ImageCms.DESCRIPTION.strip().startswith("pyCMS") diff --git a/docs/deprecations.rst b/docs/deprecations.rst index 3cd37925a..366360dbe 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -130,6 +130,14 @@ ICNS (width, height, scale) sizes Setting an ICNS image size to ``(width, height, scale)`` before loading has been deprecated. Instead, ``load(scale)`` can be used. +Image isImageType() +^^^^^^^^^^^^^^^^^^^ + +.. deprecated:: 11.0.0 + +``Image.isImageType(im)`` has been deprecated. Use ``isinstance(im, Image.Image)`` +instead. + ImageMath.lambda_eval and ImageMath.unsafe_eval options parameter ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/docs/releasenotes/11.0.0.rst b/docs/releasenotes/11.0.0.rst index 9e36ad06a..f53b4cca1 100644 --- a/docs/releasenotes/11.0.0.rst +++ b/docs/releasenotes/11.0.0.rst @@ -69,6 +69,14 @@ ICNS (width, height, scale) sizes Setting an ICNS image size to ``(width, height, scale)`` before loading has been deprecated. Instead, ``load(scale)`` can be used. +Image isImageType() +^^^^^^^^^^^^^^^^^^^ + +.. deprecated:: 11.0.0 + +``Image.isImageType(im)`` has been deprecated. Use ``isinstance(im, Image.Image)`` +instead. + ImageMath.lambda_eval and ImageMath.unsafe_eval options parameter ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 07de45f3a..8f16eef94 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -133,6 +133,7 @@ def isImageType(t: Any) -> TypeGuard[Image]: :param t: object to check if it's an image :returns: True if the object is an image """ + deprecate("Image.isImageType(im)", 12, "isinstance(im, Image.Image)") return hasattr(t, "im") @@ -1130,17 +1131,23 @@ class Image: return new_im if "LAB" in (self.mode, mode): - other_mode = mode if self.mode == "LAB" else self.mode + im = self + if mode == "LAB": + if im.mode not in ("RGB", "RGBA", "RGBX"): + im = im.convert("RGBA") + other_mode = im.mode + else: + other_mode = mode if other_mode in ("RGB", "RGBA", "RGBX"): from . import ImageCms srgb = ImageCms.createProfile("sRGB") lab = ImageCms.createProfile("LAB") - profiles = [lab, srgb] if self.mode == "LAB" else [srgb, lab] + profiles = [lab, srgb] if im.mode == "LAB" else [srgb, lab] transform = ImageCms.buildTransform( - profiles[0], profiles[1], self.mode, mode + profiles[0], profiles[1], im.mode, mode ) - return transform.apply(self) + return transform.apply(im) # colorspace conversion if dither is None: @@ -1817,23 +1824,22 @@ class Image: :param mask: An optional mask image. """ - if isImageType(box): + if isinstance(box, Image): if mask is not None: msg = "If using second argument as mask, third argument must be None" raise ValueError(msg) # abbreviated paste(im, mask) syntax mask = box box = None - assert not isinstance(box, Image) if box is None: box = (0, 0) if len(box) == 2: # upper left corner given; get size from image or mask - if isImageType(im): + if isinstance(im, Image): size = im.size - elif isImageType(mask): + elif isinstance(mask, Image): size = mask.size else: # FIXME: use self.size here? @@ -1846,17 +1852,15 @@ class Image: from . import ImageColor source = ImageColor.getcolor(im, self.mode) - elif isImageType(im): + elif isinstance(im, Image): im.load() if self.mode != im.mode: if self.mode != "RGB" or im.mode not in ("LA", "RGBA", "RGBa"): # should use an adapter for this! im = im.convert(self.mode) source = im.im - elif isinstance(im, tuple): - source = im else: - source = cast(float, im) + source = im self._ensure_mutable() @@ -2017,7 +2021,7 @@ class Image: else: band = 3 - if isImageType(alpha): + if isinstance(alpha, Image): # alpha layer if alpha.mode not in ("1", "L"): msg = "illegal image mode" @@ -2027,7 +2031,6 @@ class Image: alpha = alpha.convert("L") else: # constant alpha - alpha = cast(int, alpha) # see python/typing#1013 try: self.im.fillband(band, alpha) except (AttributeError, ValueError): diff --git a/src/PIL/ImageMath.py b/src/PIL/ImageMath.py index 191cc2a5f..191ca8524 100644 --- a/src/PIL/ImageMath.py +++ b/src/PIL/ImageMath.py @@ -268,7 +268,7 @@ def lambda_eval( args.update(options) args.update(kw) for k, v in args.items(): - if hasattr(v, "im"): + if isinstance(v, Image.Image): args[k] = _Operand(v) out = expression(args) @@ -319,7 +319,7 @@ def unsafe_eval( args.update(options) args.update(kw) for k, v in args.items(): - if hasattr(v, "im"): + if isinstance(v, Image.Image): args[k] = _Operand(v) compiled_code = compile(expression, "", "eval") diff --git a/src/libImaging/GetBBox.c b/src/libImaging/GetBBox.c index c61a27d3b..d430893dd 100644 --- a/src/libImaging/GetBBox.c +++ b/src/libImaging/GetBBox.c @@ -30,26 +30,57 @@ ImagingGetBBox(Imaging im, int bbox[4], int alpha_only) { bbox[1] = -1; bbox[2] = bbox[3] = 0; -#define GETBBOX(image, mask) \ - for (y = 0; y < im->ysize; y++) { \ - has_data = 0; \ - for (x = 0; x < im->xsize; x++) { \ - if (im->image[y][x] & mask) { \ - has_data = 1; \ - if (x < bbox[0]) { \ - bbox[0] = x; \ - } \ - if (x >= bbox[2]) { \ - bbox[2] = x + 1; \ - } \ - } \ - } \ - if (has_data) { \ - if (bbox[1] < 0) { \ - bbox[1] = y; \ - } \ - bbox[3] = y + 1; \ - } \ +#define GETBBOX(image, mask) \ + /* first stage: looking for any pixels from top */ \ + for (y = 0; y < im->ysize; y++) { \ + has_data = 0; \ + for (x = 0; x < im->xsize; x++) { \ + if (im->image[y][x] & mask) { \ + has_data = 1; \ + bbox[0] = x; \ + bbox[1] = y; \ + break; \ + } \ + } \ + if (has_data) { \ + break; \ + } \ + } \ + /* Check that we have a box */ \ + if (bbox[1] < 0) { \ + return 0; /* no data */ \ + } \ + /* second stage: looking for any pixels from bottom */ \ + for (y = im->ysize - 1; y >= bbox[1]; y--) { \ + has_data = 0; \ + for (x = 0; x < im->xsize; x++) { \ + if (im->image[y][x] & mask) { \ + has_data = 1; \ + if (x < bbox[0]) { \ + bbox[0] = x; \ + } \ + bbox[3] = y + 1; \ + break; \ + } \ + } \ + if (has_data) { \ + break; \ + } \ + } \ + /* third stage: looking for left and right boundaries */ \ + for (y = bbox[1]; y < bbox[3]; y++) { \ + for (x = 0; x < bbox[0]; x++) { \ + if (im->image[y][x] & mask) { \ + bbox[0] = x; \ + break; \ + } \ + } \ + for (x = im->xsize - 1; x >= bbox[2]; x--) { \ + if (im->image[y][x] & mask) { \ + bbox[2] = x + 1; \ + break; \ + } \ + } \ } if (im->image8) { @@ -71,11 +102,6 @@ ImagingGetBBox(Imaging im, int bbox[4], int alpha_only) { GETBBOX(image32, mask); } - /* Check that we got a box */ - if (bbox[1] < 0) { - return 0; /* no data */ - } - return 1; /* ok */ } @@ -144,6 +170,9 @@ ImagingGetExtrema(Imaging im, void *extrema) { imax = in[x]; } } + if (imin == 0 && imax == 255) { + break; + } } ((UINT8 *)extrema)[0] = (UINT8)imin; ((UINT8 *)extrema)[1] = (UINT8)imax;