diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh
index c79cd2f17..cbd8534aa 100755
--- a/.github/workflows/wheels-dependencies.sh
+++ b/.github/workflows/wheels-dependencies.sh
@@ -96,13 +96,13 @@ ARCHIVE_SDIR=pillow-depends-main
FREETYPE_VERSION=2.13.3
HARFBUZZ_VERSION=11.3.3
LIBPNG_VERSION=1.6.50
-JPEGTURBO_VERSION=3.1.1
+JPEGTURBO_VERSION=3.1.2
OPENJPEG_VERSION=2.5.3
XZ_VERSION=5.8.1
+ZSTD_VERSION=1.5.7
TIFF_VERSION=4.7.0
LCMS2_VERSION=2.17
-ZLIB_VERSION=1.3.1
-ZLIB_NG_VERSION=2.2.4
+ZLIB_NG_VERSION=2.2.5
LIBWEBP_VERSION=1.6.0
BZIP2_VERSION=1.0.8
LIBXCB_VERSION=1.17.0
@@ -254,16 +254,20 @@ function build_libavif {
touch libavif-stamp
}
+function build_zstd {
+ if [ -e zstd-stamp ]; then return; fi
+ local out_dir=$(fetch_unpack https://github.com/facebook/zstd/releases/download/v$ZSTD_VERSION/zstd-$ZSTD_VERSION.tar.gz)
+ (cd $out_dir \
+ && make -j4 install)
+ touch zstd-stamp
+}
+
function build {
build_xz
if [ -z "$IS_ALPINE" ] && [ -z "$SANITIZER" ] && [ -z "$IS_MACOS" ]; then
yum remove -y zlib-devel
fi
- if [[ -n "$IS_MACOS" ]] && [[ "$MACOSX_DEPLOYMENT_TARGET" == "10.10" || "$MACOSX_DEPLOYMENT_TARGET" == "10.13" ]]; then
- build_new_zlib
- else
- build_zlib_ng
- fi
+ build_zlib_ng
build_simple xcb-proto 1.17.0 https://xorg.freedesktop.org/archive/individual/proto
if [[ -n "$IS_MACOS" ]]; then
@@ -285,6 +289,7 @@ function build {
--with-jpeg-include-dir=$BUILD_PREFIX/include --with-jpeg-lib-dir=$BUILD_PREFIX/lib \
--disable-webp --disable-libdeflate --disable-zstd
else
+ build_zstd
build_tiff
fi
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 2be509d54..23bda1ec7 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,6 +1,6 @@
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
- rev: v0.12.7
+ rev: v0.12.11
hooks:
- id: ruff-check
args: [--exit-non-zero-on-fix]
@@ -24,7 +24,7 @@ repos:
exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.gd$|\.opt$|\.patch$)
- repo: https://github.com/pre-commit/mirrors-clang-format
- rev: v20.1.8
+ rev: v21.1.0
hooks:
- id: clang-format
types: [c]
@@ -36,7 +36,7 @@ repos:
- id: rst-backticks
- repo: https://github.com/pre-commit/pre-commit-hooks
- rev: v5.0.0
+ rev: v6.0.0
hooks:
- id: check-executables-have-shebangs
- id: check-shebang-scripts-are-executable
@@ -51,14 +51,14 @@ repos:
exclude: ^.github/.*TEMPLATE|^Tests/(fonts|images)/|\.patch$
- repo: https://github.com/python-jsonschema/check-jsonschema
- rev: 0.33.2
+ rev: 0.33.3
hooks:
- id: check-github-workflows
- id: check-readthedocs
- id: check-renovate
- repo: https://github.com/zizmorcore/zizmor-pre-commit
- rev: v1.11.0
+ rev: v1.12.1
hooks:
- id: zizmor
diff --git a/Tests/test_file_gbr.py b/Tests/test_file_gbr.py
index 1b834cd3c..b8851d82b 100644
--- a/Tests/test_file_gbr.py
+++ b/Tests/test_file_gbr.py
@@ -1,8 +1,10 @@
from __future__ import annotations
+from io import BytesIO
+
import pytest
-from PIL import GbrImagePlugin, Image
+from PIL import GbrImagePlugin, Image, _binary
from .helper import assert_image_equal_tofile
@@ -31,8 +33,49 @@ def test_multiple_load_operations() -> None:
assert_image_equal_tofile(im, "Tests/images/gbr.png")
-def test_invalid_file() -> None:
- invalid_file = "Tests/images/flower.jpg"
+def create_gbr_image(info: dict[str, int] = {}, magic_number=b"") -> BytesIO:
+ return BytesIO(
+ b"".join(
+ _binary.o32be(i)
+ for i in [
+ info.get("header_size", 20),
+ info.get("version", 1),
+ info.get("width", 1),
+ info.get("height", 1),
+ info.get("color_depth", 1),
+ ]
+ )
+ + magic_number
+ )
- with pytest.raises(SyntaxError):
+
+def test_invalid_file() -> None:
+ for f in [
+ create_gbr_image({"header_size": 0}),
+ create_gbr_image({"width": 0}),
+ create_gbr_image({"height": 0}),
+ ]:
+ with pytest.raises(SyntaxError, match="not a GIMP brush"):
+ GbrImagePlugin.GbrImageFile(f)
+
+ invalid_file = "Tests/images/flower.jpg"
+ with pytest.raises(SyntaxError, match="Unsupported GIMP brush version"):
GbrImagePlugin.GbrImageFile(invalid_file)
+
+
+def test_unsupported_gimp_brush() -> None:
+ f = create_gbr_image({"color_depth": 2})
+ with pytest.raises(SyntaxError, match="Unsupported GIMP brush color depth: 2"):
+ GbrImagePlugin.GbrImageFile(f)
+
+
+def test_bad_magic_number() -> None:
+ f = create_gbr_image({"version": 2}, magic_number=b"badm")
+ with pytest.raises(SyntaxError, match="not a GIMP brush, bad magic number"):
+ GbrImagePlugin.GbrImageFile(f)
+
+
+def test_L() -> None:
+ f = create_gbr_image()
+ with Image.open(f) as im:
+ assert im.mode == "L"
diff --git a/Tests/test_file_iptc.py b/Tests/test_file_iptc.py
index 3c4c892c8..5a8aaa3ef 100644
--- a/Tests/test_file_iptc.py
+++ b/Tests/test_file_iptc.py
@@ -2,6 +2,8 @@ from __future__ import annotations
from io import BytesIO
+import pytest
+
from PIL import Image, IptcImagePlugin, TiffImagePlugin, TiffTags
from .helper import assert_image_equal, hopper
@@ -9,21 +11,78 @@ from .helper import assert_image_equal, hopper
TEST_FILE = "Tests/images/iptc.jpg"
+def create_iptc_image(info: dict[str, int] = {}) -> BytesIO:
+ def field(tag, value):
+ return bytes((0x1C,) + tag + (0, len(value))) + value
+
+ data = field((3, 60), bytes((info.get("layers", 1), info.get("component", 0))))
+ data += field((3, 120), bytes((info.get("compression", 1),)))
+ if "band" in info:
+ data += field((3, 65), bytes((info["band"] + 1,)))
+ data += field((3, 20), b"\x01") # width
+ data += field((3, 30), b"\x01") # height
+ data += field(
+ (8, 10),
+ bytes((info.get("data", 0),)),
+ )
+
+ return BytesIO(data)
+
+
def test_open() -> None:
expected = Image.new("L", (1, 1))
- f = BytesIO(
- b"\x1c\x03<\x00\x02\x01\x00\x1c\x03x\x00\x01\x01\x1c\x03\x14\x00\x01\x01"
- b"\x1c\x03\x1e\x00\x01\x01\x1c\x08\n\x00\x01\x00"
- )
+ f = create_iptc_image()
with Image.open(f) as im:
- assert im.tile == [("iptc", (0, 0, 1, 1), 25, "raw")]
+ assert im.tile == [("iptc", (0, 0, 1, 1), 25, ("raw", None))]
assert_image_equal(im, expected)
with Image.open(f) as im:
assert im.load() is not None
+def test_field_length() -> None:
+ f = create_iptc_image()
+ f.seek(28)
+ f.write(b"\xff")
+ with pytest.raises(OSError, match="illegal field length in IPTC/NAA file"):
+ with Image.open(f):
+ pass
+
+
+@pytest.mark.parametrize("layers, mode", ((3, "RGB"), (4, "CMYK")))
+def test_layers(layers: int, mode: str) -> None:
+ for band in range(-1, layers):
+ info = {"layers": layers, "component": 1, "data": 5}
+ if band != -1:
+ info["band"] = band
+ f = create_iptc_image(info)
+ with Image.open(f) as im:
+ assert im.mode == mode
+
+ data = [0] * layers
+ data[max(band, 0)] = 5
+ assert im.getpixel((0, 0)) == tuple(data)
+
+
+def test_unknown_compression() -> None:
+ f = create_iptc_image({"compression": 2})
+ with pytest.raises(OSError, match="Unknown IPTC image compression"):
+ with Image.open(f):
+ pass
+
+
+def test_getiptcinfo() -> None:
+ f = create_iptc_image()
+ with Image.open(f) as im:
+ assert IptcImagePlugin.getiptcinfo(im) == {
+ (3, 60): b"\x01\x00",
+ (3, 120): b"\x01",
+ (3, 20): b"\x01",
+ (3, 30): b"\x01",
+ }
+
+
def test_getiptcinfo_jpg_none() -> None:
# Arrange
with hopper() as im:
diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py
index e5e36dbb2..9dc3fc628 100644
--- a/Tests/test_file_libtiff.py
+++ b/Tests/test_file_libtiff.py
@@ -373,8 +373,7 @@ class TestFileLibTiff(LibTiffTestCase):
with Image.open(out) as reloaded:
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
- if 700 in reloaded.tag_v2:
- assert reloaded.tag_v2[700] == b"xmlpacket tag"
+ assert reloaded.tag_v2[700] == b"xmlpacket tag"
def test_int_dpi(self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
# issue #1765
diff --git a/Tests/test_file_pcd.py b/Tests/test_file_pcd.py
index 81a316fc1..9bf1a75f0 100644
--- a/Tests/test_file_pcd.py
+++ b/Tests/test_file_pcd.py
@@ -1,10 +1,15 @@
from __future__ import annotations
+from io import BytesIO
+
+import pytest
+
from PIL import Image
def test_load_raw() -> None:
with Image.open("Tests/images/hopper.pcd") as im:
+ assert im.size == (768, 512)
im.load() # should not segfault.
# Note that this image was created with a resized hopper
@@ -15,3 +20,13 @@ def test_load_raw() -> None:
# target = hopper().resize((768,512))
# assert_image_similar(im, target, 10)
+
+
+@pytest.mark.parametrize("orientation", (1, 3))
+def test_rotated(orientation: int) -> None:
+ with open("Tests/images/hopper.pcd", "rb") as fp:
+ data = bytearray(fp.read())
+ data[2048 + 1538] = orientation
+ f = BytesIO(data)
+ with Image.open(f) as im:
+ assert im.size == (512, 768)
diff --git a/Tests/test_file_wal.py b/Tests/test_file_wal.py
index b15d79d61..549d47054 100644
--- a/Tests/test_file_wal.py
+++ b/Tests/test_file_wal.py
@@ -1,5 +1,7 @@
from __future__ import annotations
+from io import BytesIO
+
from PIL import WalImageFile
from .helper import assert_image_equal_tofile
@@ -13,12 +15,22 @@ def test_open() -> None:
assert im.format_description == "Quake2 Texture"
assert im.mode == "P"
assert im.size == (128, 128)
+ assert "next_name" not in im.info
assert isinstance(im, WalImageFile.WalImageFile)
assert_image_equal_tofile(im, "Tests/images/hopper_wal.png")
+def test_next_name() -> None:
+ with open(TEST_FILE, "rb") as fp:
+ data = bytearray(fp.read())
+ data[56:60] = b"Test"
+ f = BytesIO(data)
+ with WalImageFile.open(f) as im:
+ assert im.info["next_name"] == b"Test"
+
+
def test_load() -> None:
with WalImageFile.open(TEST_FILE) as im:
px = im.load()
diff --git a/Tests/test_font_crash.py b/Tests/test_font_crash.py
index b82340ef7..54bd2d183 100644
--- a/Tests/test_font_crash.py
+++ b/Tests/test_font_crash.py
@@ -9,7 +9,8 @@ from .helper import skip_unless_feature
class TestFontCrash:
def _fuzz_font(self, font: ImageFont.FreeTypeFont) -> None:
- # from fuzzers.fuzz_font
+ # Copy of the code from fuzz_font() in Tests/oss-fuzz/fuzzers.py
+ # that triggered a problem when fuzzing
font.getbbox("ABC")
font.getmask("test text")
with Image.new(mode="RGBA", size=(200, 200)) as im:
diff --git a/Tests/test_image.py b/Tests/test_image.py
index 83b027aa2..eb3882ddc 100644
--- a/Tests/test_image.py
+++ b/Tests/test_image.py
@@ -19,6 +19,7 @@ from PIL import (
ImageDraw,
ImageFile,
ImagePalette,
+ ImageShow,
UnidentifiedImageError,
features,
)
@@ -388,6 +389,37 @@ class TestImage:
assert img_colors is not None
assert sorted(img_colors) == expected_colors
+ def test_alpha_composite_la(self) -> None:
+ # Arrange
+ expected_colors = sorted(
+ [
+ (3300, (255, 255)),
+ (1156, (170, 192)),
+ (1122, (128, 255)),
+ (1089, (0, 0)),
+ (1122, (255, 128)),
+ (1122, (0, 128)),
+ (1089, (0, 255)),
+ ]
+ )
+
+ dst = Image.new("LA", size=(100, 100), color=(0, 255))
+ draw = ImageDraw.Draw(dst)
+ draw.rectangle((0, 33, 100, 66), fill=(0, 128))
+ draw.rectangle((0, 67, 100, 100), fill=(0, 0))
+ src = Image.new("LA", size=(100, 100), color=(255, 255))
+ draw = ImageDraw.Draw(src)
+ draw.rectangle((33, 0, 66, 100), fill=(255, 128))
+ draw.rectangle((67, 0, 100, 100), fill=(255, 0))
+
+ # Act
+ img = Image.alpha_composite(dst, src)
+
+ # Assert
+ img_colors = img.getcolors()
+ assert img_colors is not None
+ assert sorted(img_colors) == expected_colors
+
def test_alpha_inplace(self) -> None:
src = Image.new("RGBA", (128, 128), "blue")
@@ -922,6 +954,17 @@ class TestImage:
reloaded_exif.load(exif.tobytes())
assert reloaded_exif.get_ifd(0x8769) == exif.get_ifd(0x8769)
+ def test_delete_ifd_tag(self) -> None:
+ with Image.open("Tests/images/flower.jpg") as im:
+ exif = im.getexif()
+ exif.get_ifd(0x8769)
+ assert 0x8769 in exif
+ del exif[0x8769]
+
+ reloaded_exif = Image.Exif()
+ reloaded_exif.load(exif.tobytes())
+ assert 0x8769 not in reloaded_exif
+
def test_exif_load_from_fp(self) -> None:
with Image.open("Tests/images/flower.jpg") as im:
data = im.info["exif"]
@@ -1005,6 +1048,13 @@ class TestImage:
with pytest.warns(DeprecationWarning, match="Image.Image.get_child_images"):
assert im.get_child_images() == []
+ def test_show(self, monkeypatch: pytest.MonkeyPatch) -> None:
+ monkeypatch.setattr(ImageShow, "_viewers", [])
+
+ im = Image.new("RGB", (1, 1))
+ with pytest.warns(DeprecationWarning, match="Image._show"):
+ Image._show(im)
+
@pytest.mark.parametrize("size", ((1, 0), (0, 1), (0, 0)))
def test_zero_tobytes(self, size: tuple[int, int]) -> None:
im = Image.new("RGB", size)
diff --git a/Tests/test_imagemorph.py b/Tests/test_imagemorph.py
index 515e29cea..ca192a809 100644
--- a/Tests/test_imagemorph.py
+++ b/Tests/test_imagemorph.py
@@ -7,7 +7,7 @@ import pytest
from PIL import Image, ImageMorph, _imagingmorph
-from .helper import assert_image_equal_tofile, hopper
+from .helper import assert_image_equal_tofile, hopper, timeout_unless_slower_valgrind
def string_to_img(image_string: str) -> Image.Image:
@@ -266,16 +266,18 @@ def test_unknown_pattern() -> None:
ImageMorph.LutBuilder(op_name="unknown")
-def test_pattern_syntax_error() -> None:
+@pytest.mark.parametrize(
+ "pattern", ("a pattern with a syntax error", "4:(" + "X" * 30000)
+)
+@timeout_unless_slower_valgrind(1)
+def test_pattern_syntax_error(pattern: str) -> None:
# Arrange
lb = ImageMorph.LutBuilder(op_name="corner")
- new_patterns = ["a pattern with a syntax error"]
+ new_patterns = [pattern]
lb.add_patterns(new_patterns)
# Act / Assert
- with pytest.raises(
- Exception, match='Syntax error in pattern "a pattern with a syntax error"'
- ):
+ with pytest.raises(Exception, match='Syntax error in pattern "'):
lb.build_lut()
diff --git a/Tests/test_imagestat.py b/Tests/test_imagestat.py
index 0dfbc5a2a..0baab7ce2 100644
--- a/Tests/test_imagestat.py
+++ b/Tests/test_imagestat.py
@@ -57,3 +57,13 @@ def test_constant() -> None:
assert st.rms[0] == 128
assert st.var[0] == 0
assert st.stddev[0] == 0
+
+
+def test_zero_count() -> None:
+ im = Image.new("L", (0, 0))
+
+ st = ImageStat.Stat(im)
+
+ assert st.mean == [0]
+ assert st.rms == [0]
+ assert st.var == [0]
diff --git a/Tests/test_pyroma.py b/Tests/test_pyroma.py
index 35f3fd076..5871a7213 100644
--- a/Tests/test_pyroma.py
+++ b/Tests/test_pyroma.py
@@ -9,7 +9,7 @@ from PIL import __version__
pyroma = pytest.importorskip("pyroma", reason="Pyroma not installed")
-def map_metadata_keys(metadata):
+def map_metadata_keys(md):
# Convert installed wheel metadata into canonical Core Metadata 2.4 format.
# This was a utility method in pyroma 4.3.3; it was removed in 5.0.
# This implementation is constructed from the relevant logic from
@@ -17,8 +17,8 @@ def map_metadata_keys(metadata):
# upstream to Pyroma as https://github.com/regebro/pyroma/pull/116,
# so it may be possible to simplify this test in future.
data = {}
- for key in set(metadata.keys()):
- value = metadata.get_all(key)
+ for key in set(md.keys()):
+ value = md.get_all(key)
key = pyroma.projectdata.normalize(key)
if len(value) == 1:
diff --git a/checks/check_wheel.py b/checks/check_wheel.py
index 937722c4b..f716c8498 100644
--- a/checks/check_wheel.py
+++ b/checks/check_wheel.py
@@ -4,7 +4,6 @@ import platform
import sys
from PIL import features
-from Tests.helper import is_pypy
def test_wheel_modules() -> None:
@@ -48,8 +47,6 @@ def test_wheel_features() -> None:
if sys.platform == "win32":
expected_features.remove("xcb")
- elif sys.platform == "darwin" and not is_pypy() and platform.processor() != "arm":
- expected_features.remove("zlib_ng")
elif sys.platform == "ios":
# Can't distribute raqm due to licensing, and there's no system version;
# fribidi and harfbuzz won't be available if raqm isn't available.
diff --git a/depends/install_raqm.sh b/depends/install_raqm.sh
index 5d862403e..b5a05100b 100755
--- a/depends/install_raqm.sh
+++ b/depends/install_raqm.sh
@@ -2,7 +2,7 @@
# install raqm
-archive=libraqm-0.10.2
+archive=libraqm-0.10.3
./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz
diff --git a/docs/deprecations.rst b/docs/deprecations.rst
index 3f95cf7f5..e31d3c31c 100644
--- a/docs/deprecations.rst
+++ b/docs/deprecations.rst
@@ -61,6 +61,14 @@ ImageCms.ImageCmsProfile.product_name and .product_info
``.product_info`` attributes have been deprecated, and will be removed in
Pillow 13 (2026-10-15). They have been set to ``None`` since Pillow 2.3.0.
+Image._show
+~~~~~~~~~~~
+
+.. deprecated:: 12.0.0
+
+``Image._show`` has been deprecated, and will be removed in Pillow 13 (2026-10-15).
+Use :py:meth:`~PIL.ImageShow.show` instead.
+
Removed features
----------------
diff --git a/docs/releasenotes/12.0.0.rst b/docs/releasenotes/12.0.0.rst
index e21c243ea..12bf760e2 100644
--- a/docs/releasenotes/12.0.0.rst
+++ b/docs/releasenotes/12.0.0.rst
@@ -116,6 +116,12 @@ vulnerability introduced in FreeType 2.6 (:cve:`2020-15999`).
Deprecations
============
+Image._show
+^^^^^^^^^^^
+
+``Image._show`` has been deprecated, and will be removed in Pillow 13 (2026-10-15).
+Use :py:meth:`~PIL.ImageShow.show` instead.
+
ImageCms.ImageCmsProfile.product_name and .product_info
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
@@ -150,3 +156,10 @@ others prepare for 3.14, and to ensure Pillow could be used immediately at the r
of 3.14.0 final (2025-10-07, :pep:`745`).
Pillow 12.0.0 now officially supports Python 3.14.
+
+ImageMorph operations must have length 1
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Valid ImageMorph operations are 4, N, 1 and M. By limiting the length to 1 character
+within Pillow, long execution times can be avoided if a user provided long pattern
+strings. Reported by Jang Choi.
diff --git a/src/PIL/FliImagePlugin.py b/src/PIL/FliImagePlugin.py
index 94ffbb1c7..add9e9914 100644
--- a/src/PIL/FliImagePlugin.py
+++ b/src/PIL/FliImagePlugin.py
@@ -30,7 +30,7 @@ from ._util import DeferredError
def _accept(prefix: bytes) -> bool:
return (
- len(prefix) >= 6
+ len(prefix) >= 16
and i16(prefix, 4) in [0xAF11, 0xAF12]
and i16(prefix, 14) in [0, 3] # flags
)
diff --git a/src/PIL/GbrImagePlugin.py b/src/PIL/GbrImagePlugin.py
index 556310bd6..ec666c81c 100644
--- a/src/PIL/GbrImagePlugin.py
+++ b/src/PIL/GbrImagePlugin.py
@@ -55,7 +55,7 @@ class GbrImageFile(ImageFile.ImageFile):
width = i32(self.fp.read(4))
height = i32(self.fp.read(4))
color_depth = i32(self.fp.read(4))
- if width <= 0 or height <= 0:
+ if width == 0 or height == 0:
msg = "not a GIMP brush"
raise SyntaxError(msg)
if color_depth not in (1, 4):
@@ -72,7 +72,7 @@ class GbrImageFile(ImageFile.ImageFile):
raise SyntaxError(msg)
self.info["spacing"] = i32(self.fp.read(4))
- comment = self.fp.read(comment_length)[:-1]
+ self.info["comment"] = self.fp.read(comment_length)[:-1]
if color_depth == 1:
self._mode = "L"
@@ -81,8 +81,6 @@ class GbrImageFile(ImageFile.ImageFile):
self._size = width, height
- self.info["comment"] = comment
-
# Image might not be small
Image._decompression_bomb_check(self.size)
diff --git a/src/PIL/GribStubImagePlugin.py b/src/PIL/GribStubImagePlugin.py
index 59ba572c7..146a6fa0d 100644
--- a/src/PIL/GribStubImagePlugin.py
+++ b/src/PIL/GribStubImagePlugin.py
@@ -33,7 +33,7 @@ def register_handler(handler: ImageFile.StubHandler | None) -> None:
def _accept(prefix: bytes) -> bool:
- return prefix.startswith(b"GRIB") and prefix[7] == 1
+ return len(prefix) >= 8 and prefix.startswith(b"GRIB") and prefix[7] == 1
class GribStubImageFile(ImageFile.StubImageFile):
diff --git a/src/PIL/Image.py b/src/PIL/Image.py
index 683c80762..5a457803b 100644
--- a/src/PIL/Image.py
+++ b/src/PIL/Image.py
@@ -2632,7 +2632,9 @@ class Image:
:param title: Optional title to use for the image window, where possible.
"""
- _show(self, title=title)
+ from . import ImageShow
+
+ ImageShow.show(self, title)
def split(self) -> tuple[Image, ...]:
"""
@@ -3570,9 +3572,8 @@ def alpha_composite(im1: Image, im2: Image) -> Image:
"""
Alpha composite im2 over im1.
- :param im1: The first image. Must have mode RGBA.
- :param im2: The second image. Must have mode RGBA, and the same size as
- the first image.
+ :param im1: The first image. Must have mode RGBA or LA.
+ :param im2: The second image. Must have the same mode and size as the first image.
:returns: An :py:class:`~PIL.Image.Image` object.
"""
@@ -3798,6 +3799,7 @@ def register_encoder(name: str, encoder: type[ImageFile.PyEncoder]) -> None:
def _show(image: Image, **options: Any) -> None:
from . import ImageShow
+ deprecate("Image._show", 13, "ImageShow.show")
ImageShow.show(image, **options)
@@ -4219,6 +4221,8 @@ class Exif(_ExifBase):
del self._info[tag]
else:
del self._data[tag]
+ if tag in self._ifds:
+ del self._ifds[tag]
def __iter__(self) -> Iterator[int]:
keys = set(self._data)
diff --git a/src/PIL/ImageMorph.py b/src/PIL/ImageMorph.py
index f0a066b5b..bd70aff7b 100644
--- a/src/PIL/ImageMorph.py
+++ b/src/PIL/ImageMorph.py
@@ -150,7 +150,7 @@ class LutBuilder:
# Parse and create symmetries of the patterns strings
for p in self.patterns:
- m = re.search(r"(\w*):?\s*\((.+?)\)\s*->\s*(\d)", p.replace("\n", ""))
+ m = re.search(r"(\w):?\s*\((.+?)\)\s*->\s*(\d)", p.replace("\n", ""))
if not m:
msg = 'Syntax error in pattern "' + p + '"'
raise Exception(msg)
diff --git a/src/PIL/ImageStat.py b/src/PIL/ImageStat.py
index 8bc504526..3a1044ba4 100644
--- a/src/PIL/ImageStat.py
+++ b/src/PIL/ImageStat.py
@@ -120,7 +120,7 @@ class Stat:
@cached_property
def mean(self) -> list[float]:
"""Average (arithmetic mean) pixel level for each band in the image."""
- return [self.sum[i] / self.count[i] for i in self.bands]
+ return [self.sum[i] / self.count[i] if self.count[i] else 0 for i in self.bands]
@cached_property
def median(self) -> list[int]:
@@ -141,13 +141,20 @@ class Stat:
@cached_property
def rms(self) -> list[float]:
"""RMS (root-mean-square) for each band in the image."""
- return [math.sqrt(self.sum2[i] / self.count[i]) for i in self.bands]
+ return [
+ math.sqrt(self.sum2[i] / self.count[i]) if self.count[i] else 0
+ for i in self.bands
+ ]
@cached_property
def var(self) -> list[float]:
"""Variance for each band in the image."""
return [
- (self.sum2[i] - (self.sum[i] ** 2.0) / self.count[i]) / self.count[i]
+ (
+ (self.sum2[i] - (self.sum[i] ** 2.0) / self.count[i]) / self.count[i]
+ if self.count[i]
+ else 0
+ )
for i in self.bands
]
diff --git a/src/PIL/IptcImagePlugin.py b/src/PIL/IptcImagePlugin.py
index d9ced4549..e63c65253 100644
--- a/src/PIL/IptcImagePlugin.py
+++ b/src/PIL/IptcImagePlugin.py
@@ -34,10 +34,6 @@ def _i(c: bytes) -> int:
return i32((b"\0\0\0\0" + c)[-4:])
-def _i8(c: int | bytes) -> int:
- return c if isinstance(c, int) else c[0]
-
-
##
# Image plugin for IPTC/NAA datastreams. To read IPTC/NAA fields
# from TIFF and JPEG files, use the getiptcinfo function.
@@ -102,16 +98,18 @@ class IptcImageFile(ImageFile.ImageFile):
# mode
layers = self.info[(3, 60)][0]
component = self.info[(3, 60)][1]
- if (3, 65) in self.info:
- id = self.info[(3, 65)][0] - 1
- else:
- id = 0
if layers == 1 and not component:
self._mode = "L"
- elif layers == 3 and component:
- self._mode = "RGB"[id]
- elif layers == 4 and component:
- self._mode = "CMYK"[id]
+ band = None
+ else:
+ if layers == 3 and component:
+ self._mode = "RGB"
+ elif layers == 4 and component:
+ self._mode = "CMYK"
+ if (3, 65) in self.info:
+ band = self.info[(3, 65)][0] - 1
+ else:
+ band = 0
# size
self._size = self.getint((3, 20)), self.getint((3, 30))
@@ -126,40 +124,45 @@ class IptcImageFile(ImageFile.ImageFile):
# tile
if tag == (8, 10):
self.tile = [
- ImageFile._Tile("iptc", (0, 0) + self.size, offset, compression)
+ ImageFile._Tile("iptc", (0, 0) + self.size, offset, (compression, band))
]
def load(self) -> Image.core.PixelAccess | None:
- if len(self.tile) != 1 or self.tile[0][0] != "iptc":
- return ImageFile.ImageFile.load(self)
+ if self.tile:
+ args = self.tile[0].args
+ assert isinstance(args, tuple)
+ compression, band = args
- offset, compression = self.tile[0][2:]
+ assert self.fp is not None
+ self.fp.seek(self.tile[0].offset)
- assert self.fp is not None
- self.fp.seek(offset)
-
- # Copy image data to temporary file
- o = BytesIO()
- if compression == "raw":
- # To simplify access to the extracted file,
- # prepend a PPM header
- o.write(b"P5\n%d %d\n255\n" % self.size)
- while True:
- type, size = self.field()
- if type != (8, 10):
- break
- while size > 0:
- s = self.fp.read(min(size, 8192))
- if not s:
+ # Copy image data to temporary file
+ o = BytesIO()
+ if compression == "raw":
+ # To simplify access to the extracted file,
+ # prepend a PPM header
+ o.write(b"P5\n%d %d\n255\n" % self.size)
+ while True:
+ type, size = self.field()
+ if type != (8, 10):
break
- o.write(s)
- size -= len(s)
+ while size > 0:
+ s = self.fp.read(min(size, 8192))
+ if not s:
+ break
+ o.write(s)
+ size -= len(s)
- with Image.open(o) as _im:
- _im.load()
- self.im = _im.im
- self.tile = []
- return Image.Image.load(self)
+ with Image.open(o) as _im:
+ if band is not None:
+ bands = [Image.new("L", _im.size)] * Image.getmodebands(self.mode)
+ bands[band] = _im
+ _im = Image.merge(self.mode, bands)
+ else:
+ _im.load()
+ self.im = _im.im
+ self.tile = []
+ return ImageFile.ImageFile.load(self)
Image.register_open(IptcImageFile.format, IptcImageFile)
diff --git a/src/PIL/PcdImagePlugin.py b/src/PIL/PcdImagePlugin.py
index 3aa249988..7f9ab525c 100644
--- a/src/PIL/PcdImagePlugin.py
+++ b/src/PIL/PcdImagePlugin.py
@@ -32,7 +32,7 @@ class PcdImageFile(ImageFile.ImageFile):
assert self.fp is not None
self.fp.seek(2048)
- s = self.fp.read(2048)
+ s = self.fp.read(1539)
if not s.startswith(b"PCD_"):
msg = "not a PCD file"
@@ -46,14 +46,13 @@ class PcdImageFile(ImageFile.ImageFile):
self.tile_post_rotate = -90
self._mode = "RGB"
- self._size = 768, 512 # FIXME: not correct for rotated images!
+ self._size = (512, 768) if orientation in (1, 3) else (768, 512)
self.tile = [ImageFile._Tile("pcd", (0, 0) + self.size, 96 * 2048)]
def load_end(self) -> None:
if self.tile_post_rotate:
# Handle rotated PCDs
self.im = self.im.rotate(self.tile_post_rotate)
- self._size = self.im.size
#
diff --git a/src/PIL/PcxImagePlugin.py b/src/PIL/PcxImagePlugin.py
index 458d586c4..6b16d5385 100644
--- a/src/PIL/PcxImagePlugin.py
+++ b/src/PIL/PcxImagePlugin.py
@@ -39,7 +39,7 @@ logger = logging.getLogger(__name__)
def _accept(prefix: bytes) -> bool:
- return prefix[0] == 10 and prefix[1] in [0, 2, 3, 5]
+ return len(prefix) >= 2 and prefix[0] == 10 and prefix[1] in [0, 2, 3, 5]
##
diff --git a/src/PIL/PpmImagePlugin.py b/src/PIL/PpmImagePlugin.py
index db34d107a..307bc97ff 100644
--- a/src/PIL/PpmImagePlugin.py
+++ b/src/PIL/PpmImagePlugin.py
@@ -47,7 +47,7 @@ MODES = {
def _accept(prefix: bytes) -> bool:
- return prefix.startswith(b"P") and prefix[1] in b"0123456fy"
+ return len(prefix) >= 2 and prefix.startswith(b"P") and prefix[1] in b"0123456fy"
##
diff --git a/src/PIL/WalImageFile.py b/src/PIL/WalImageFile.py
index 7e967ff14..fb3e1c06a 100644
--- a/src/PIL/WalImageFile.py
+++ b/src/PIL/WalImageFile.py
@@ -50,8 +50,7 @@ class WalImageFile(ImageFile.ImageFile):
# strings are null-terminated
self.info["name"] = header[:32].split(b"\0", 1)[0]
- next_name = header[56 : 56 + 32].split(b"\0", 1)[0]
- if next_name:
+ if next_name := header[56 : 56 + 32].split(b"\0", 1)[0]:
self.info["next_name"] = next_name
def load(self) -> Image.core.PixelAccess | None:
diff --git a/src/decode.c b/src/decode.c
index 03db1ce35..e7a6e6323 100644
--- a/src/decode.c
+++ b/src/decode.c
@@ -870,8 +870,6 @@ PyImaging_Jpeg2KDecoderNew(PyObject *self, PyObject *args) {
if (strcmp(format, "j2k") == 0) {
codec_format = OPJ_CODEC_J2K;
- } else if (strcmp(format, "jpt") == 0) {
- codec_format = OPJ_CODEC_JPT;
} else if (strcmp(format, "jp2") == 0) {
codec_format = OPJ_CODEC_JP2;
} else {
diff --git a/src/libImaging/AlphaComposite.c b/src/libImaging/AlphaComposite.c
index 6d728f908..44c451679 100644
--- a/src/libImaging/AlphaComposite.c
+++ b/src/libImaging/AlphaComposite.c
@@ -25,13 +25,12 @@ ImagingAlphaComposite(Imaging imDst, Imaging imSrc) {
int x, y;
/* Check arguments */
- if (!imDst || !imSrc || strcmp(imDst->mode, "RGBA") ||
- imDst->type != IMAGING_TYPE_UINT8 || imDst->bands != 4) {
+ if (!imDst || !imSrc ||
+ (strcmp(imDst->mode, "RGBA") && strcmp(imDst->mode, "LA"))) {
return ImagingError_ModeError();
}
- if (strcmp(imDst->mode, imSrc->mode) || imDst->type != imSrc->type ||
- imDst->bands != imSrc->bands || imDst->xsize != imSrc->xsize ||
+ if (strcmp(imDst->mode, imSrc->mode) || imDst->xsize != imSrc->xsize ||
imDst->ysize != imSrc->ysize) {
return ImagingError_Mismatch();
}
diff --git a/src/libImaging/Palette.c b/src/libImaging/Palette.c
index 78916bca5..da1d80504 100644
--- a/src/libImaging/Palette.c
+++ b/src/libImaging/Palette.c
@@ -148,7 +148,7 @@ ImagingPaletteDelete(ImagingPalette palette) {
#define BOX 8
-#define BOXVOLUME BOX *BOX *BOX
+#define BOXVOLUME BOX * BOX * BOX
void
ImagingPaletteCacheUpdate(ImagingPalette palette, int r, int g, int b) {
diff --git a/src/thirdparty/raqm/COPYING b/src/thirdparty/raqm/COPYING
index 97e2489b7..964318a8a 100644
--- a/src/thirdparty/raqm/COPYING
+++ b/src/thirdparty/raqm/COPYING
@@ -1,7 +1,7 @@
The MIT License (MIT)
Copyright © 2015 Information Technology Authority (ITA)
-Copyright © 2016-2023 Khaled Hosny
+Copyright © 2016-2025 Khaled Hosny
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/src/thirdparty/raqm/NEWS b/src/thirdparty/raqm/NEWS
index e8bf32e0b..fb432cffb 100644
--- a/src/thirdparty/raqm/NEWS
+++ b/src/thirdparty/raqm/NEWS
@@ -1,3 +1,19 @@
+Overview of changes leading to 0.10.3
+Tuesday, August 5, 2025
+====================================
+
+Fix raqm_set_text_utf8/utf16 reading beyond len for multibyte.
+
+Support building against SheenBidi 2.9.
+
+Fix deprecation warning with latest HarfBuzz.
+
+Overview of changes leading to 0.10.2
+Sunday, September 22, 2024
+====================================
+
+Fix Unicode codepoint conversion from UTF-16.
+
Overview of changes leading to 0.10.1
Wednesday, April 12, 2023
====================================
diff --git a/src/thirdparty/raqm/raqm-version.h b/src/thirdparty/raqm/raqm-version.h
index 62d2d2064..f2dd61cf6 100644
--- a/src/thirdparty/raqm/raqm-version.h
+++ b/src/thirdparty/raqm/raqm-version.h
@@ -33,9 +33,9 @@
#define RAQM_VERSION_MAJOR 0
#define RAQM_VERSION_MINOR 10
-#define RAQM_VERSION_MICRO 1
+#define RAQM_VERSION_MICRO 3
-#define RAQM_VERSION_STRING "0.10.1"
+#define RAQM_VERSION_STRING "0.10.3"
#define RAQM_VERSION_ATLEAST(major,minor,micro) \
((major)*10000+(minor)*100+(micro) <= \
diff --git a/src/thirdparty/raqm/raqm.c b/src/thirdparty/raqm/raqm.c
index 2b331e1af..9ecc5cac8 100644
--- a/src/thirdparty/raqm/raqm.c
+++ b/src/thirdparty/raqm/raqm.c
@@ -30,7 +30,11 @@
#include
#ifdef RAQM_SHEENBIDI
+#ifdef RAQM_SHEENBIDI_GT_2_9
+#include
+#else
#include
+#endif
#else
#ifdef HAVE_FRIBIDI_SYSTEM
#include
@@ -546,34 +550,32 @@ raqm_set_text (raqm_t *rq,
return true;
}
-static void *
-_raqm_get_utf8_codepoint (const void *str,
+static const char *
+_raqm_get_utf8_codepoint (const char *str,
uint32_t *out_codepoint)
{
- const char *s = (const char *)str;
-
- if (0xf0 == (0xf8 & s[0]))
+ if (0xf0 == (0xf8 & str[0]))
{
- *out_codepoint = ((0x07 & s[0]) << 18) | ((0x3f & s[1]) << 12) | ((0x3f & s[2]) << 6) | (0x3f & s[3]);
- s += 4;
+ *out_codepoint = ((0x07 & str[0]) << 18) | ((0x3f & str[1]) << 12) | ((0x3f & str[2]) << 6) | (0x3f & str[3]);
+ str += 4;
}
- else if (0xe0 == (0xf0 & s[0]))
+ else if (0xe0 == (0xf0 & str[0]))
{
- *out_codepoint = ((0x0f & s[0]) << 12) | ((0x3f & s[1]) << 6) | (0x3f & s[2]);
- s += 3;
+ *out_codepoint = ((0x0f & str[0]) << 12) | ((0x3f & str[1]) << 6) | (0x3f & str[2]);
+ str += 3;
}
- else if (0xc0 == (0xe0 & s[0]))
+ else if (0xc0 == (0xe0 & str[0]))
{
- *out_codepoint = ((0x1f & s[0]) << 6) | (0x3f & s[1]);
- s += 2;
+ *out_codepoint = ((0x1f & str[0]) << 6) | (0x3f & str[1]);
+ str += 2;
}
else
{
- *out_codepoint = s[0];
- s += 1;
+ *out_codepoint = str[0];
+ str += 1;
}
- return (void *)s;
+ return str;
}
static size_t
@@ -585,42 +587,41 @@ _raqm_u8_to_u32 (const char *text, size_t len, uint32_t *unicode)
while ((*in_utf8 != '\0') && (in_len < len))
{
- in_utf8 = _raqm_get_utf8_codepoint (in_utf8, out_utf32);
+ const char *out_utf8 = _raqm_get_utf8_codepoint (in_utf8, out_utf32);
+ in_len += out_utf8 - in_utf8;
+ in_utf8 = out_utf8;
++out_utf32;
- ++in_len;
}
return (out_utf32 - unicode);
}
-static void *
-_raqm_get_utf16_codepoint (const void *str,
- uint32_t *out_codepoint)
+static const uint16_t *
+_raqm_get_utf16_codepoint (const uint16_t *str,
+ uint32_t *out_codepoint)
{
- const uint16_t *s = (const uint16_t *)str;
-
- if (s[0] > 0xD800 && s[0] < 0xDBFF)
+ if (str[0] >= 0xD800 && str[0] <= 0xDBFF)
{
- if (s[1] > 0xDC00 && s[1] < 0xDFFF)
+ if (str[1] >= 0xDC00 && str[1] <= 0xDFFF)
{
- uint32_t X = ((s[0] & ((1 << 6) -1)) << 10) | (s[1] & ((1 << 10) -1));
- uint32_t W = (s[0] >> 6) & ((1 << 5) - 1);
+ uint32_t X = ((str[0] & ((1 << 6) -1)) << 10) | (str[1] & ((1 << 10) -1));
+ uint32_t W = (str[0] >> 6) & ((1 << 5) - 1);
*out_codepoint = (W+1) << 16 | X;
- s += 2;
+ str += 2;
}
else
{
/* A single high surrogate, this is an error. */
- *out_codepoint = s[0];
- s += 1;
+ *out_codepoint = str[0];
+ str += 1;
}
}
else
{
- *out_codepoint = s[0];
- s += 1;
+ *out_codepoint = str[0];
+ str += 1;
}
- return (void *)s;
+ return str;
}
static size_t
@@ -632,9 +633,10 @@ _raqm_u16_to_u32 (const uint16_t *text, size_t len, uint32_t *unicode)
while ((*in_utf16 != '\0') && (in_len < len))
{
- in_utf16 = _raqm_get_utf16_codepoint (in_utf16, out_utf32);
+ const uint16_t *out_utf16 = _raqm_get_utf16_codepoint (in_utf16, out_utf32);
+ in_len += (out_utf16 - in_utf16);
+ in_utf16 = out_utf16;
++out_utf32;
- ++in_len;
}
return (out_utf32 - unicode);
@@ -1114,12 +1116,12 @@ _raqm_set_spacing (raqm_t *rq,
{
if (_raqm_allowed_grapheme_boundary (rq->text[i], rq->text[i+1]))
{
- /* CSS word seperators, word spacing is only applied on these.*/
+ /* CSS word separators, word spacing is only applied on these.*/
if (rq->text[i] == 0x0020 || /* Space */
rq->text[i] == 0x00A0 || /* No Break Space */
rq->text[i] == 0x1361 || /* Ethiopic Word Space */
- rq->text[i] == 0x10100 || /* Aegean Word Seperator Line */
- rq->text[i] == 0x10101 || /* Aegean Word Seperator Dot */
+ rq->text[i] == 0x10100 || /* Aegean Word Separator Line */
+ rq->text[i] == 0x10101 || /* Aegean Word Separator Dot */
rq->text[i] == 0x1039F || /* Ugaric Word Divider */
rq->text[i] == 0x1091F) /* Phoenician Word Separator */
{
@@ -2167,6 +2169,10 @@ _raqm_ft_transform (int *x,
*y = vector.y;
}
+#if !HB_VERSION_ATLEAST (10, 4, 0)
+# define hb_ft_font_get_ft_face hb_ft_font_get_face
+#endif
+
static bool
_raqm_shape (raqm_t *rq)
{
@@ -2199,7 +2205,7 @@ _raqm_shape (raqm_t *rq)
hb_glyph_position_t *pos;
unsigned int len;
- FT_Get_Transform (hb_ft_font_get_face (run->font), &matrix, NULL);
+ FT_Get_Transform (hb_ft_font_get_ft_face (run->font), &matrix, NULL);
pos = hb_buffer_get_glyph_positions (run->buffer, &len);
info = hb_buffer_get_glyph_infos (run->buffer, &len);
diff --git a/wheels/dependency_licenses/ZSTD.txt b/wheels/dependency_licenses/ZSTD.txt
new file mode 100644
index 000000000..75800288c
--- /dev/null
+++ b/wheels/dependency_licenses/ZSTD.txt
@@ -0,0 +1,30 @@
+BSD License
+
+For Zstandard software
+
+Copyright (c) Meta Platforms, Inc. and affiliates. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+ * Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+
+ * Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+ * Neither the name Facebook, nor Meta, nor the names of its contributors may
+ be used to endorse or promote products derived from this software without
+ specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/wheels/multibuild b/wheels/multibuild
index 42d761728..647393271 160000
--- a/wheels/multibuild
+++ b/wheels/multibuild
@@ -1 +1 @@
-Subproject commit 42d761728d141d8462cd9943f4329f12fe62b155
+Subproject commit 64739327166fcad1fa41ad9b23fa910fa244c84f
diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py
index 5633519dd..4ba683801 100644
--- a/winbuild/build_prepare.py
+++ b/winbuild/build_prepare.py
@@ -117,7 +117,7 @@ V = {
"FREETYPE": "2.13.3",
"FRIBIDI": "1.0.16",
"HARFBUZZ": "11.3.3",
- "JPEGTURBO": "3.1.1",
+ "JPEGTURBO": "3.1.2",
"LCMS2": "2.17",
"LIBAVIF": "1.3.0",
"LIBIMAGEQUANT": "4.4.0",
@@ -126,7 +126,7 @@ V = {
"OPENJPEG": "2.5.3",
"TIFF": "4.7.0",
"XZ": "5.8.1",
- "ZLIBNG": "2.2.4",
+ "ZLIBNG": "2.2.5",
}
V["LIBPNG_XY"] = "".join(V["LIBPNG"].split(".")[:2])