Skip to content

Commit

Permalink
Merge pull request #8689 from radarhere/get_child_images
Browse files Browse the repository at this point in the history
  • Loading branch information
hugovk authored Jan 17, 2025
2 parents 85a6df5 + be8e55d commit 4f7510b
Show file tree
Hide file tree
Showing 7 changed files with 131 additions and 44 deletions.
5 changes: 5 additions & 0 deletions Tests/test_image.py
Original file line number Diff line number Diff line change
Expand Up @@ -989,6 +989,11 @@ def test_getxmp_padded(self) -> None:
else:
assert im.getxmp() == {"xmpmeta": None}

def test_get_child_images(self) -> None:
im = Image.new("RGB", (1, 1))
with pytest.warns(DeprecationWarning):
assert im.get_child_images() == []

@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)
Expand Down
10 changes: 10 additions & 0 deletions docs/deprecations.rst
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,16 @@ ExifTags.IFD.Makernote
``ExifTags.IFD.Makernote`` has been deprecated. Instead, use
``ExifTags.IFD.MakerNote``.

Image.Image.get_child_images()
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

.. deprecated:: 11.2.0

``Image.Image.get_child_images()`` has been deprecated. and will be removed in Pillow
13 (2026-10-15). It will be moved to ``ImageFile.ImageFile.get_child_images()``. The
method uses an image's file pointer, and so child images could only be retrieved from
an :py:class:`PIL.ImageFile.ImageFile` instance.

Removed features
----------------

Expand Down
58 changes: 58 additions & 0 deletions docs/releasenotes/11.2.0.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
11.2.0
------

Security
========

TODO
^^^^

TODO

:cve:`YYYY-XXXXX`: TODO
^^^^^^^^^^^^^^^^^^^^^^^

TODO

Backwards Incompatible Changes
==============================

TODO
^^^^

Deprecations
============

Image.Image.get_child_images()
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

.. deprecated:: 11.2.0

``Image.Image.get_child_images()`` has been deprecated. and will be removed in Pillow
13 (2026-10-15). It will be moved to ``ImageFile.ImageFile.get_child_images()``. The
method uses an image's file pointer, and so child images could only be retrieved from
an :py:class:`PIL.ImageFile.ImageFile` instance.

API Changes
===========

TODO
^^^^

TODO

API Additions
=============

TODO
^^^^

TODO

Other Changes
=============

TODO
^^^^

TODO
1 change: 1 addition & 0 deletions docs/releasenotes/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ expected to be backported to earlier versions.
.. toctree::
:maxdepth: 2

11.2.0
11.1.0
11.0.0
10.4.0
Expand Down
46 changes: 3 additions & 43 deletions src/PIL/Image.py
Original file line number Diff line number Diff line change
Expand Up @@ -1554,50 +1554,10 @@ def _reload_exif(self) -> None:
self.getexif()

def get_child_images(self) -> list[ImageFile.ImageFile]:
child_images = []
exif = self.getexif()
ifds = []
if ExifTags.Base.SubIFDs in exif:
subifd_offsets = exif[ExifTags.Base.SubIFDs]
if subifd_offsets:
if not isinstance(subifd_offsets, tuple):
subifd_offsets = (subifd_offsets,)
for subifd_offset in subifd_offsets:
ifds.append((exif._get_ifd_dict(subifd_offset), subifd_offset))
ifd1 = exif.get_ifd(ExifTags.IFD.IFD1)
if ifd1 and ifd1.get(ExifTags.Base.JpegIFOffset):
assert exif._info is not None
ifds.append((ifd1, exif._info.next))

offset = None
for ifd, ifd_offset in ifds:
current_offset = self.fp.tell()
if offset is None:
offset = current_offset

fp = self.fp
if ifd is not None:
thumbnail_offset = ifd.get(ExifTags.Base.JpegIFOffset)
if thumbnail_offset is not None:
thumbnail_offset += getattr(self, "_exif_offset", 0)
self.fp.seek(thumbnail_offset)
data = self.fp.read(ifd.get(ExifTags.Base.JpegIFByteCount))
fp = io.BytesIO(data)

with open(fp) as im:
from . import TiffImagePlugin

if thumbnail_offset is None and isinstance(
im, TiffImagePlugin.TiffImageFile
):
im._frame_pos = [ifd_offset]
im._seek(0)
im.load()
child_images.append(im)
from . import ImageFile

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

def getim(self) -> CapsuleType:
"""
Expand Down
53 changes: 52 additions & 1 deletion src/PIL/ImageFile.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
import sys
from typing import IO, TYPE_CHECKING, Any, NamedTuple, cast

from . import Image
from . import ExifTags, Image
from ._deprecate import deprecate
from ._util import is_path

Expand Down Expand Up @@ -163,6 +163,57 @@ def __init__(
def _open(self) -> None:
pass

def get_child_images(self) -> list[ImageFile]:
child_images = []
exif = self.getexif()
ifds = []
if ExifTags.Base.SubIFDs in exif:
subifd_offsets = exif[ExifTags.Base.SubIFDs]
if subifd_offsets:
if not isinstance(subifd_offsets, tuple):
subifd_offsets = (subifd_offsets,)
for subifd_offset in subifd_offsets:
ifds.append((exif._get_ifd_dict(subifd_offset), subifd_offset))
ifd1 = exif.get_ifd(ExifTags.IFD.IFD1)
if ifd1 and ifd1.get(ExifTags.Base.JpegIFOffset):
assert exif._info is not None
ifds.append((ifd1, exif._info.next))

offset = None
for ifd, ifd_offset in ifds:
assert self.fp is not None
current_offset = self.fp.tell()
if offset is None:
offset = current_offset

fp = self.fp
if ifd is not None:
thumbnail_offset = ifd.get(ExifTags.Base.JpegIFOffset)
if thumbnail_offset is not None:
thumbnail_offset += getattr(self, "_exif_offset", 0)
self.fp.seek(thumbnail_offset)

length = ifd.get(ExifTags.Base.JpegIFByteCount)
assert isinstance(length, int)
data = self.fp.read(length)
fp = io.BytesIO(data)

with Image.open(fp) as im:
from . import TiffImagePlugin

if thumbnail_offset is None and isinstance(
im, TiffImagePlugin.TiffImageFile
):
im._frame_pos = [ifd_offset]
im._seek(0)
im.load()
child_images.append(im)

if offset is not None:
assert self.fp is not None
self.fp.seek(offset)
return child_images

def get_format_mimetype(self) -> str | None:
if self.custom_mimetype:
return self.custom_mimetype
Expand Down
2 changes: 2 additions & 0 deletions src/PIL/_deprecate.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ def deprecate(
raise RuntimeError(msg)
elif when == 12:
removed = "Pillow 12 (2025-10-15)"
elif when == 13:
removed = "Pillow 13 (2026-10-15)"
else:
msg = f"Unknown removal version: {when}. Update {__name__}?"
raise ValueError(msg)
Expand Down

0 comments on commit 4f7510b

Please sign in to comment.