Skip to content

Commit 4f7510b

Browse files
authored
Merge pull request #8689 from radarhere/get_child_images
2 parents 85a6df5 + be8e55d commit 4f7510b

File tree

7 files changed

+131
-44
lines changed

7 files changed

+131
-44
lines changed

Tests/test_image.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -989,6 +989,11 @@ def test_getxmp_padded(self) -> None:
989989
else:
990990
assert im.getxmp() == {"xmpmeta": None}
991991

992+
def test_get_child_images(self) -> None:
993+
im = Image.new("RGB", (1, 1))
994+
with pytest.warns(DeprecationWarning):
995+
assert im.get_child_images() == []
996+
992997
@pytest.mark.parametrize("size", ((1, 0), (0, 1), (0, 0)))
993998
def test_zero_tobytes(self, size: tuple[int, int]) -> None:
994999
im = Image.new("RGB", size)

docs/deprecations.rst

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,16 @@ ExifTags.IFD.Makernote
183183
``ExifTags.IFD.Makernote`` has been deprecated. Instead, use
184184
``ExifTags.IFD.MakerNote``.
185185

186+
Image.Image.get_child_images()
187+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
188+
189+
.. deprecated:: 11.2.0
190+
191+
``Image.Image.get_child_images()`` has been deprecated. and will be removed in Pillow
192+
13 (2026-10-15). It will be moved to ``ImageFile.ImageFile.get_child_images()``. The
193+
method uses an image's file pointer, and so child images could only be retrieved from
194+
an :py:class:`PIL.ImageFile.ImageFile` instance.
195+
186196
Removed features
187197
----------------
188198

docs/releasenotes/11.2.0.rst

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
11.2.0
2+
------
3+
4+
Security
5+
========
6+
7+
TODO
8+
^^^^
9+
10+
TODO
11+
12+
:cve:`YYYY-XXXXX`: TODO
13+
^^^^^^^^^^^^^^^^^^^^^^^
14+
15+
TODO
16+
17+
Backwards Incompatible Changes
18+
==============================
19+
20+
TODO
21+
^^^^
22+
23+
Deprecations
24+
============
25+
26+
Image.Image.get_child_images()
27+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
28+
29+
.. deprecated:: 11.2.0
30+
31+
``Image.Image.get_child_images()`` has been deprecated. and will be removed in Pillow
32+
13 (2026-10-15). It will be moved to ``ImageFile.ImageFile.get_child_images()``. The
33+
method uses an image's file pointer, and so child images could only be retrieved from
34+
an :py:class:`PIL.ImageFile.ImageFile` instance.
35+
36+
API Changes
37+
===========
38+
39+
TODO
40+
^^^^
41+
42+
TODO
43+
44+
API Additions
45+
=============
46+
47+
TODO
48+
^^^^
49+
50+
TODO
51+
52+
Other Changes
53+
=============
54+
55+
TODO
56+
^^^^
57+
58+
TODO

docs/releasenotes/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ expected to be backported to earlier versions.
1414
.. toctree::
1515
:maxdepth: 2
1616

17+
11.2.0
1718
11.1.0
1819
11.0.0
1920
10.4.0

src/PIL/Image.py

Lines changed: 3 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1554,50 +1554,10 @@ def _reload_exif(self) -> None:
15541554
self.getexif()
15551555

15561556
def get_child_images(self) -> list[ImageFile.ImageFile]:
1557-
child_images = []
1558-
exif = self.getexif()
1559-
ifds = []
1560-
if ExifTags.Base.SubIFDs in exif:
1561-
subifd_offsets = exif[ExifTags.Base.SubIFDs]
1562-
if subifd_offsets:
1563-
if not isinstance(subifd_offsets, tuple):
1564-
subifd_offsets = (subifd_offsets,)
1565-
for subifd_offset in subifd_offsets:
1566-
ifds.append((exif._get_ifd_dict(subifd_offset), subifd_offset))
1567-
ifd1 = exif.get_ifd(ExifTags.IFD.IFD1)
1568-
if ifd1 and ifd1.get(ExifTags.Base.JpegIFOffset):
1569-
assert exif._info is not None
1570-
ifds.append((ifd1, exif._info.next))
1571-
1572-
offset = None
1573-
for ifd, ifd_offset in ifds:
1574-
current_offset = self.fp.tell()
1575-
if offset is None:
1576-
offset = current_offset
1577-
1578-
fp = self.fp
1579-
if ifd is not None:
1580-
thumbnail_offset = ifd.get(ExifTags.Base.JpegIFOffset)
1581-
if thumbnail_offset is not None:
1582-
thumbnail_offset += getattr(self, "_exif_offset", 0)
1583-
self.fp.seek(thumbnail_offset)
1584-
data = self.fp.read(ifd.get(ExifTags.Base.JpegIFByteCount))
1585-
fp = io.BytesIO(data)
1586-
1587-
with open(fp) as im:
1588-
from . import TiffImagePlugin
1589-
1590-
if thumbnail_offset is None and isinstance(
1591-
im, TiffImagePlugin.TiffImageFile
1592-
):
1593-
im._frame_pos = [ifd_offset]
1594-
im._seek(0)
1595-
im.load()
1596-
child_images.append(im)
1557+
from . import ImageFile
15971558

1598-
if offset is not None:
1599-
self.fp.seek(offset)
1600-
return child_images
1559+
deprecate("Image.Image.get_child_images", 13)
1560+
return ImageFile.ImageFile.get_child_images(self) # type: ignore[arg-type]
16011561

16021562
def getim(self) -> CapsuleType:
16031563
"""

src/PIL/ImageFile.py

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
import sys
3737
from typing import IO, TYPE_CHECKING, Any, NamedTuple, cast
3838

39-
from . import Image
39+
from . import ExifTags, Image
4040
from ._deprecate import deprecate
4141
from ._util import is_path
4242

@@ -163,6 +163,57 @@ def __init__(
163163
def _open(self) -> None:
164164
pass
165165

166+
def get_child_images(self) -> list[ImageFile]:
167+
child_images = []
168+
exif = self.getexif()
169+
ifds = []
170+
if ExifTags.Base.SubIFDs in exif:
171+
subifd_offsets = exif[ExifTags.Base.SubIFDs]
172+
if subifd_offsets:
173+
if not isinstance(subifd_offsets, tuple):
174+
subifd_offsets = (subifd_offsets,)
175+
for subifd_offset in subifd_offsets:
176+
ifds.append((exif._get_ifd_dict(subifd_offset), subifd_offset))
177+
ifd1 = exif.get_ifd(ExifTags.IFD.IFD1)
178+
if ifd1 and ifd1.get(ExifTags.Base.JpegIFOffset):
179+
assert exif._info is not None
180+
ifds.append((ifd1, exif._info.next))
181+
182+
offset = None
183+
for ifd, ifd_offset in ifds:
184+
assert self.fp is not None
185+
current_offset = self.fp.tell()
186+
if offset is None:
187+
offset = current_offset
188+
189+
fp = self.fp
190+
if ifd is not None:
191+
thumbnail_offset = ifd.get(ExifTags.Base.JpegIFOffset)
192+
if thumbnail_offset is not None:
193+
thumbnail_offset += getattr(self, "_exif_offset", 0)
194+
self.fp.seek(thumbnail_offset)
195+
196+
length = ifd.get(ExifTags.Base.JpegIFByteCount)
197+
assert isinstance(length, int)
198+
data = self.fp.read(length)
199+
fp = io.BytesIO(data)
200+
201+
with Image.open(fp) as im:
202+
from . import TiffImagePlugin
203+
204+
if thumbnail_offset is None and isinstance(
205+
im, TiffImagePlugin.TiffImageFile
206+
):
207+
im._frame_pos = [ifd_offset]
208+
im._seek(0)
209+
im.load()
210+
child_images.append(im)
211+
212+
if offset is not None:
213+
assert self.fp is not None
214+
self.fp.seek(offset)
215+
return child_images
216+
166217
def get_format_mimetype(self) -> str | None:
167218
if self.custom_mimetype:
168219
return self.custom_mimetype

src/PIL/_deprecate.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ def deprecate(
4747
raise RuntimeError(msg)
4848
elif when == 12:
4949
removed = "Pillow 12 (2025-10-15)"
50+
elif when == 13:
51+
removed = "Pillow 13 (2026-10-15)"
5052
else:
5153
msg = f"Unknown removal version: {when}. Update {__name__}?"
5254
raise ValueError(msg)

0 commit comments

Comments
 (0)