Repository: pydicom/pydicom

Issue:
GDCM with v2 pixels backend fails to decode multi-frame JPEG
**Describe the bug**
While testing #2226 with GDCM I found that the v2 pixel data backend is failing to decode the user's multi-frame multi-sample dataset:

```python
Traceback (most recent call last):
  File "/.../issues/2226/2226.py", line 20, in <module>
    arr = ds.pixel_array
  File "/.../pydicom/src/pydicom/dataset.py", line 916, in __getattr__
    return object.__getattribute__(self, name)
  File "/.../pydicom/src/pydicom/dataset.py", line 2221, in pixel_array
    self.convert_pixel_data()
  File "/.../pydicom/src/pydicom/dataset.py", line 1750, in convert_pixel_data
    self._convert_pixel_data_without_handler()
  File "/.../pydicom/src/pydicom/dataset.py", line 1857, in _convert_pixel_data_without_handler
    raise last_exception  # type: ignore[misc]
  File "/.../pydicom/src/pydicom/dataset.py", line 1838, in _convert_pixel_data_without_handler
    self._do_pixel_data_conversion(handler)
  File "/.../pydicom/src/pydicom/dataset.py", line 1864, in _do_pixel_data_conversion
    arr = handler.get_pixeldata(self)
  File "/.../src/pydicom/pixel_data_handlers/gdcm_handler.py", line 266, in get_pixeldata
    pixel_bytearray = cast(bytes, pixel_str.encode("utf-8", "surrogateescape"))
AttributeError: 'SwigPyObject' object has no attribute 'encode'
```




Discussion:
May be related to #1754 and the respective [GDCM issue](https://sourceforge.net/p/gdcm/bugs/551/).
It appears the failure (on my test system) starts once the decoded data is > 2^31 bytes, which makes me think there might be an i32 limitation somewhere.

Decoding with GDCM by itself is OK, SWIG/Python seems to be the cause.
From what I can tell GDCM defines `GetBuffer()` in [gdcmswig.i](https://github.com/malaterre/GDCM/blob/master/Wrapping/Python/gdcmswig.i) as:
```
%extend gdcm::Bitmap
{
  // http://mail.python.org/pipermail/python-list/2006-January/361540.html
  %cstring_output_allocate_size(char **buffer, unsigned int *size, free(*$1) );
  void GetBuffer(char **buffer, unsigned int *size) {
    *size = self->GetBufferLength();
    *buffer = (char*)malloc(*size);
    bool ret = self->GetBuffer(*buffer);
    if (!ret) {
      free(*buffer);
      *buffer = 0;
      *size = 0;
    }
  }
};
```
But the `%cstring_output_allocate_size()` macro wraps [this function](https://github.com/swig/swig/blob/b96b955ca15a01f0425fb26c234528530923202a/Lib/ocaml/ocamlrun.swg#L366), which takes an `int` rather than `uint`, so presumably any value above `2**31 - 1` is overflowing and so failing the `len < 0` check?
I don't actually know if this is fixable, as far as I can tell there isn't a way to use SWIG to return a str larger than 2**31 - 1.

Might be best to do an expected length check and if too large raise early. Alternatively, if the expected length is too large then split the decoding into multiple steps. Or just use the v3 backend 😛 



Patch:
diff --git a/doc/release_notes/v3.1.0.rst b/doc/release_notes/v3.1.0.rst
index cdc20c548e..ff50d50735 100644
--- a/doc/release_notes/v3.1.0.rst
+++ b/doc/release_notes/v3.1.0.rst
@@ -5,6 +5,7 @@ Changes
 -------
 
 * DICOM dictionary updated to 2024d.
+* Prefer using pylibjpeg over GDCM when encoding *RLE Lossless*.
 
 Fixes
 -----
@@ -26,6 +27,8 @@ Fixes
   :class:`~pydicom.multival.MultiValue`.
 * Make sure that no download of examples is attempted on importing examples if the example
   data is not found locally (:issue:`2223`).
+* Add mitigation and better error messaging for SWIG errors when using GDCM to decompress
+  datasets with more than 2**31 - 1 bytes (:issue:`2227`)
 
 Enhancements
 ------------
diff --git a/src/pydicom/pixel_data_handlers/gdcm_handler.py b/src/pydicom/pixel_data_handlers/gdcm_handler.py
index db41d8a5a1..ed2391fec0 100644
--- a/src/pydicom/pixel_data_handlers/gdcm_handler.py
+++ b/src/pydicom/pixel_data_handlers/gdcm_handler.py
@@ -4,9 +4,7 @@
 """
 
 from copy import deepcopy
-import os
 import sys
-from tempfile import NamedTemporaryFile
 from typing import TYPE_CHECKING, cast
 
 if TYPE_CHECKING:  # pragma: no cover
@@ -25,10 +23,8 @@
     from gdcm import DataElement
 
     HAVE_GDCM = True
-    HAVE_GDCM_IN_MEMORY_SUPPORT = hasattr(DataElement, "SetByteStringValue")
 except ImportError:
     HAVE_GDCM = False
-    HAVE_GDCM_IN_MEMORY_SUPPORT = False
 
 from pydicom import config
 from pydicom.encaps import generate_frames, generate_fragmented_frames
@@ -117,28 +113,22 @@ def create_data_element(ds: "Dataset") -> "DataElement":
     gdcm.DataElement
         The converted *Pixel Data* element.
     """
-    tsyntax = ds.file_meta.TransferSyntaxUID
-    data_element = gdcm.DataElement(gdcm.Tag(0x7FE0, 0x0010))
-    if tsyntax.is_compressed:
-        fragments = gdcm.SequenceOfFragments.New()
-        nr_frames = get_nr_frames(ds, warn=False)
-        fragment_gen = generate_fragmented_frames(
-            ds.PixelData, number_of_frames=nr_frames
-        )
-        for frame_fragments in fragment_gen:
-            for fragment_data in frame_fragments:
-                fragment = gdcm.Fragment()
-                fragment.SetByteStringValue(fragment_data)
-                fragments.AddFragment(fragment)
+    elem = gdcm.DataElement(gdcm.Tag(0x7FE0, 0x0010))
+    fragments = gdcm.SequenceOfFragments.New()
+    nr_frames = get_nr_frames(ds, warn=False)
+    fragment_gen = generate_fragmented_frames(ds.PixelData, number_of_frames=nr_frames)
+    for frame_fragments in fragment_gen:
+        for fragment_data in frame_fragments:
+            fragment = gdcm.Fragment()
+            fragment.SetByteStringValue(fragment_data)
+            fragments.AddFragment(fragment)
 
-        data_element.SetValue(fragments.__ref__())
-    else:
-        data_element.SetByteStringValue(ds.PixelData)
+    elem.SetValue(fragments.GetPointer())
 
-    return data_element
+    return elem
 
 
-def create_image(ds: "Dataset", data_element: "DataElement") -> "gdcm.Image":
+def create_image(ds: "Dataset") -> "gdcm.Image":
     """Return a ``gdcm.Image``.
 
     Parameters
@@ -146,8 +136,6 @@ def create_image(ds: "Dataset", data_element: "DataElement") -> "gdcm.Image":
     ds : dataset.Dataset
         The :class:`~pydicom.dataset.Dataset` containing the Image
         Pixel module.
-    data_element : gdcm.DataElement
-        The ``gdcm.DataElement`` *Pixel Data* element.
 
     Returns
     -------
@@ -157,7 +145,6 @@ def create_image(ds: "Dataset", data_element: "DataElement") -> "gdcm.Image":
     number_of_frames = get_nr_frames(ds, warn=False)
     image.SetNumberOfDimensions(2 if number_of_frames == 1 else 3)
     image.SetDimensions((ds.Columns, ds.Rows, number_of_frames))
-    image.SetDataElement(data_element)
 
     pi_type = gdcm.PhotometricInterpretation.GetPIType(ds.PhotometricInterpretation)
     image.SetPhotometricInterpretation(gdcm.PhotometricInterpretation(pi_type))
@@ -179,50 +166,13 @@ def create_image(ds: "Dataset", data_element: "DataElement") -> "gdcm.Image":
     return image
 
 
-def _get_pixel_str_fileio(ds: "Dataset") -> str:
-    """Return the pixel data from `ds` as a str.
-
-    Used for GDCM < 2.8.8.
-
-    Parameters
-    ----------
-    ds : pydicom.dataset.Dataset
-        The dataset to create the str from.
-
-    Returns
-    -------
-    str
-        The UTF-8 encoded pixel data.
-    """
-    reader = gdcm.ImageReader()
-    fname = getattr(ds, "filename", None)
-    if fname and isinstance(fname, str):
-        reader.SetFileName(fname)
-        if not reader.Read():
-            raise TypeError("GDCM could not read DICOM image")
-
-        return cast(str, reader.GetImage().GetBuffer())
-
-    # Copy the relevant elements and write to a temporary file to avoid
-    #   having to deal with all the possible objects the dataset may
-    #   originate with
-    new = ds.group_dataset(0x0028)
-    new["PixelData"] = ds["PixelData"]  # avoid ambiguous VR
-    new.file_meta = ds.file_meta
-    with NamedTemporaryFile("wb", delete=False) as t:
-        new.save_as(t)
-
-    reader.SetFileName(t.name)
-    if not reader.Read():
-        raise TypeError("GDCM could not read DICOM image")
-
-    pixel_str: str = reader.GetImage().GetBuffer()
-
-    # Need to kill the gdcm.ImageReader to free file access
-    reader = None
-    os.remove(t.name)
-
-    return pixel_str
+# Due to SWIG issues, it appears that GDCM cannot return more than (typically)
+#   2**31 - 1 bytes when using gdcm.Image.GetBuffer(), although this may actually
+#   be as low as 2**15 - 1 bytes depending on the system architecture.
+# Because of this we cannot guarantee that GDCM will succeed as a single frame of
+#   data may be larger - however in most cases its only multi-frame data that will
+#   exceed that limit.
+_GDCM_MAX_BUFFER_SIZE = 2**31 - 1
 
 
 def get_pixeldata(ds: "Dataset") -> "numpy.ndarray":
@@ -246,21 +196,70 @@ def get_pixeldata(ds: "Dataset") -> "numpy.ndarray":
     if not HAVE_GDCM:
         raise ImportError("The GDCM handler requires both gdcm and numpy")
 
-    if HAVE_GDCM_IN_MEMORY_SUPPORT:
-        gdcm_data_element = create_data_element(ds)
-        gdcm_image = create_image(ds, gdcm_data_element)
-        pixel_str = gdcm_image.GetBuffer()
-    else:
-        pixel_str = _get_pixel_str_fileio(ds)
+    # Check to see if it's possible to decode any of the pixel data
+    frame_length = ds.Columns * ds.Rows * ds.SamplesPerPixel * ds.BitsAllocated // 8
+    if frame_length > _GDCM_MAX_BUFFER_SIZE:
+        raise ValueError(
+            "GDCM cannot decode the pixel data as each frame will be larger than "
+            "GDCM's maximum buffer size"
+        )
+
+    expected_length_bytes = get_expected_length(ds)
+    if ds.PhotometricInterpretation == "YBR_FULL_422":
+        # GDCM has already resampled the pixel data, see PS3.3 C.7.6.3.1.2
+        expected_length_bytes = expected_length_bytes // 2 * 3
+
+    # Create the gdcm.Image that will hold the encapsulated pixel data
+    image = create_image(ds)
+
+    # GDCM returns char* as type str, Python decodes this to unicode strings by default
+    # The SWIG docs mention that they always decode byte streams as utf-8 strings,
+    #   with the `surrogateescape` error handler configured.
+    # Therefore, we can encode them back to a bytearray by using the same parameters.
+    if expected_length_bytes > _GDCM_MAX_BUFFER_SIZE:
+        # Decode the pixel data in parts of no greater than _GDCM_MAX_BUFFER_SIZE
+        elem = gdcm.DataElement(gdcm.Tag(0x7FE0, 0x0010))
+        fragments = gdcm.SequenceOfFragments.New()
+        elem.SetValue(fragments.GetPointer())
+
+        buffer = bytearray()
+        frame_count = 0
+        number_of_frames = get_nr_frames(ds, warn=False)
+        frame_generator = generate_fragmented_frames(
+            ds.PixelData, number_of_frames=number_of_frames
+        )
+        for idx, frame_fragments in enumerate(frame_generator):
+            for fragment_data in frame_fragments:
+                fragment = gdcm.Fragment()
+                fragment.SetByteStringValue(fragment_data)
+                fragments.AddFragment(fragment)
 
-    # GDCM returns char* as type str. Python decodes this to
-    # unicode strings by default.
-    # The SWIG docs mention that they always decode byte streams
-    # as utf-8 strings, with the `surrogateescape`
-    # error handler configured.
-    # Therefore, we can encode them back to a bytearray
-    # by using the same parameters.
-    pixel_bytearray = cast(bytes, pixel_str.encode("utf-8", "surrogateescape"))
+            frame_count += 1
+
+            # Do a decode and reset if either:
+            # * The length of decoded pixel data will be greater than the limit on the
+            #   next iteration, or
+            # * We are at the end of the pixel data
+            if (
+                idx == number_of_frames - 1
+                or (frame_count + 1) * frame_length > _GDCM_MAX_BUFFER_SIZE
+            ):
+                image.SetNumberOfDimensions(2 if frame_count == 1 else 3)
+                image.SetDimensions((ds.Columns, ds.Rows, frame_count))
+                image.SetDataElement(elem)
+                data = cast(bytes, image.GetBuffer().encode("utf-8", "surrogateescape"))
+                buffer.extend(data)
+
+                fragments.Clear()
+                frame_count = 0
+
+        pixel_bytearray = bytes(buffer)
+    else:
+        # Decode the entire pixel data in one go
+        elem = create_data_element(ds)
+        image.SetDataElement(elem)
+        pixel_str = image.GetBuffer()
+        pixel_bytearray = cast(bytes, pixel_str.encode("utf-8", "surrogateescape"))
 
     # On big endian systems GDCM returns data as big endian :(
     if _is_big_endian_system() and ds.BitsAllocated > 8:
@@ -275,11 +274,6 @@ def get_pixeldata(ds: "Dataset") -> "numpy.ndarray":
     # Here we need to be careful because in some cases, GDCM reads a
     # buffer that is too large, so we need to make sure we only include
     # the first n_rows * n_columns * dtype_size bytes.
-    expected_length_bytes = get_expected_length(ds)
-    if ds.PhotometricInterpretation == "YBR_FULL_422":
-        # GDCM has already resampled the pixel data, see PS3.3 C.7.6.3.1.2
-        expected_length_bytes = expected_length_bytes // 2 * 3
-
     if len(pixel_bytearray) > expected_length_bytes:
         # We make sure that all the bytes after are in fact zeros
         padding = pixel_bytearray[expected_length_bytes:]
diff --git a/src/pydicom/pixels/decoders/gdcm.py b/src/pydicom/pixels/decoders/gdcm.py
index 94bcbbb4a0..b775979208 100644
--- a/src/pydicom/pixels/decoders/gdcm.py
+++ b/src/pydicom/pixels/decoders/gdcm.py
@@ -31,6 +31,14 @@
     uid.JPEG2000: ("gdcm>=3.0.10",),
 }
 
+# Due to SWIG issues, it appears that GDCM cannot return more than (typically)
+#   2**31 - 1 bytes when using gdcm.Image.GetBuffer(). Note that this may actually
+#   be as low as 2**15 - 1 bytes depending on the system architecture.
+# Because of this we cannot guarantee that GDCM will succeed as a single frame of
+#   data may be larger. However in most cases only multi-frame data that will
+#   exceed that limit.
+_GDCM_MAX_BUFFER_SIZE = 2**31 - 1
+
 
 def is_available(uid: str) -> bool:
     """Return ``True`` if a pixel data decoder for `uid` is available for use,
@@ -81,6 +89,12 @@ def _decode_frame(src: bytes, runner: DecodeRunner) -> bytes:
             "precision of 6 or 7 bits"
         )
 
+    if runner.frame_length() > _GDCM_MAX_BUFFER_SIZE:
+        raise ValueError(
+            "GDCM cannot decode the pixel data as each frame will be larger than "
+            "GDCM's maximum buffer size"
+        )
+
     fragment = gdcm.Fragment()
     fragment.SetByteStringValue(src)
 
@@ -88,7 +102,7 @@ def _decode_frame(src: bytes, runner: DecodeRunner) -> bytes:
     fragments.AddFragment(fragment)
 
     elem = gdcm.DataElement(gdcm.Tag(0x7FE0, 0x0010))
-    elem.SetValue(fragments.__ref__())
+    elem.SetValue(fragments.GetPointer())
 
     img = gdcm.Image()
     img.SetNumberOfDimensions(2)
@@ -132,6 +146,9 @@ def _decode_frame(src: bytes, runner: DecodeRunner) -> bytes:
     img.SetPixelFormat(pixel_format)
 
     # GDCM returns char* as str, so re-encode it to bytes
+    # On architectures where the C++ `int` type is 16-bit this may raise an exception
+    #   if the decoded pixel data is more than 2**15 - 1 bytes:
+    #       AttributeError: 'SwigPyObject' object has no attribute 'encode'
     frame = cast(bytes, img.GetBuffer().encode("utf-8", "surrogateescape"))
 
     # On big endian systems GDCM returns decoded data as big endian :(
diff --git a/src/pydicom/pixels/encoders/base.py b/src/pydicom/pixels/encoders/base.py
index b7fee2e766..734764cf4b 100644
--- a/src/pydicom/pixels/encoders/base.py
+++ b/src/pydicom/pixels/encoders/base.py
@@ -788,8 +788,8 @@ def iter_encode(
 RLELosslessEncoder = Encoder(RLELossless)
 RLELosslessEncoder.add_plugins(
     [
-        ("gdcm", ("pydicom.pixels.encoders.gdcm", "encode_pixel_data")),
         ("pylibjpeg", ("pydicom.pixels.encoders.pylibjpeg", "_encode_frame")),
+        ("gdcm", ("pydicom.pixels.encoders.gdcm", "encode_pixel_data")),
         ("pydicom", ("pydicom.pixels.encoders.native", "_encode_rle_frame")),
     ],
 )


Test patch:
diff --git a/tests/pixels/test_decoder_gdcm.py b/tests/pixels/test_decoder_gdcm.py
index 7d347f41d3..f57f5bba4c 100644
--- a/tests/pixels/test_decoder_gdcm.py
+++ b/tests/pixels/test_decoder_gdcm.py
@@ -16,6 +16,7 @@
 from pydicom import dcmread, config
 from pydicom.encaps import get_frame
 from pydicom.pixels import get_decoder
+from pydicom.pixels.decoders import gdcm
 from pydicom.pixels.decoders.gdcm import _decode_frame
 from pydicom.pixels.decoders.base import DecodeRunner
 from pydicom.pixels.utils import _passes_version_check
@@ -51,6 +52,15 @@ def name(ref):
     return f"{ref.name}"
 
 
+@pytest.fixture()
+def set_gdcm_max_buffer_size_10k():
+    original = gdcm._GDCM_MAX_BUFFER_SIZE
+    gdcm._GDCM_MAX_BUFFER_SIZE = 10000
+    yield
+
+    gdcm._GDCM_MAX_BUFFER_SIZE = original
+
+
 @pytest.mark.skipif(SKIP_TEST, reason="Test is missing dependencies")
 class TestDecoding:
     @pytest.mark.parametrize("reference", PIXEL_REFERENCE[JPEGBaseline8Bit], ids=name)
@@ -346,6 +356,20 @@ def foo(*args, **kwargs):
         )
         assert converted == unconverted
 
+    def test_too_large_raises(self, set_gdcm_max_buffer_size_10k):
+        """Test exception raised if the frame is too large for GDCM to decode."""
+        # Single frame is 8192 bytes with 64 x 64 x 2
+        ds = dcmread(JPGB_08_08_3_0_1F_RGB.path)
+        ds.Rows = 100
+        ds.pixel_array_options(decoding_plugin="gdcm")
+        msg = (
+            "Unable to decode as exceptions were raised by all available plugins:\n"
+            "  gdcm: GDCM cannot decode the pixel data as each frame will be larger "
+            "than GDCM's maximum buffer size"
+        )
+        with pytest.raises(RuntimeError, match=msg):
+            ds.pixel_array
+
 
 @pytest.fixture()
 def enable_debugging():
diff --git a/tests/pixels/test_encoder_base.py b/tests/pixels/test_encoder_base.py
index 33125e1ccc..88b86710b0 100644
--- a/tests/pixels/test_encoder_base.py
+++ b/tests/pixels/test_encoder_base.py
@@ -1725,8 +1725,8 @@ def test_encoder_unavailable(self, monkeypatch):
         msg = (
             r"The pixel data encoder for 'RLE Lossless' is unavailable because all "
             r"of its plugins are missing dependencies:\n"
-            r"    gdcm - requires gdcm>=3.0.10\n"
-            r"    pylibjpeg - requires numpy, pylibjpeg>=2.0 and pylibjpeg-rle>=2.0"
+            r"    pylibjpeg - requires numpy, pylibjpeg>=2.0 and pylibjpeg-rle>=2.0\n"
+            r"    gdcm - requires gdcm>=3.0.10"
         )
         with pytest.raises(RuntimeError, match=msg):
             ds.compress(RLELossless)
diff --git a/tests/pixels/test_utils.py b/tests/pixels/test_utils.py
index 737570039a..4062da3ad7 100644
--- a/tests/pixels/test_utils.py
+++ b/tests/pixels/test_utils.py
@@ -1524,8 +1524,8 @@ def test_encoder_unavailable(self, monkeypatch):
         msg = (
             r"The pixel data encoder for 'RLE Lossless' is unavailable because "
             r"all of its plugins are missing dependencies:\n"
-            r"    gdcm - requires gdcm>=3.0.10\n"
-            r"    pylibjpeg - requires numpy, pylibjpeg>=2.0 and pylibjpeg-rle>=2.0"
+            r"    pylibjpeg - requires numpy, pylibjpeg>=2.0 and pylibjpeg-rle>=2.0\n"
+            r"    gdcm - requires gdcm>=3.0.10"
         )
         with pytest.raises(RuntimeError, match=msg):
             compress(ds, RLELossless)
diff --git a/tests/test_gdcm_pixel_data.py b/tests/test_gdcm_pixel_data.py
index dbfa6bf799..fc248141bd 100644
--- a/tests/test_gdcm_pixel_data.py
+++ b/tests/test_gdcm_pixel_data.py
@@ -19,7 +19,7 @@
 import pydicom
 from pydicom.filereader import dcmread
 from pydicom.data import get_testdata_file
-from pydicom.encaps import get_frame
+from pydicom.encaps import get_frame, encapsulate
 from pydicom.pixel_data_handlers import numpy_handler, gdcm_handler
 from pydicom.pixels.processing import _convert_YBR_FULL_to_RGB
 from pydicom.pixels.utils import get_j2k_parameters, reshape_pixel_array
@@ -38,7 +38,7 @@
     RLE_32_1_1F,
 )
 
-HAVE_GDCM_IN_MEMORY_SUPPORT = gdcm_handler.HAVE_GDCM_IN_MEMORY_SUPPORT
+# HAVE_GDCM_IN_MEMORY_SUPPORT = gdcm_handler.HAVE_GDCM_IN_MEMORY_SUPPORT
 TEST_BIG_ENDIAN = sys.byteorder == "little" and HAVE_NP and HAVE_GDCM
 
 
@@ -312,31 +312,27 @@ def testJPEGlossless_pixel_array(self):
     ),
 ]
 
-with_gdcm_params = [
-    pytest.param(
-        "File", marks=pytest.mark.skipif(not HAVE_GDCM, reason=gdcm_missing_message)
-    ),
-    pytest.param(
-        "InMemory",
-        marks=pytest.mark.skipif(
-            not HAVE_GDCM_IN_MEMORY_SUPPORT, reason=gdcm_im_missing_message
-        ),
-    ),
-]
 
+@pytest.fixture()
+def set_gdcm_max_buffer_size_10k():
+    original = gdcm_handler._GDCM_MAX_BUFFER_SIZE
+    gdcm_handler._GDCM_MAX_BUFFER_SIZE = 10000
+    yield
+
+    gdcm_handler._GDCM_MAX_BUFFER_SIZE = original
 
-class TestsWithGDCM:
-    @pytest.fixture(params=with_gdcm_params, scope="class", autouse=True)
-    def with_gdcm(self, request):
-        original_value = HAVE_GDCM_IN_MEMORY_SUPPORT
-        if request.param == "File":
-            gdcm_handler.HAVE_GDCM_IN_MEMORY_SUPPORT = False
-        original_handlers = pydicom.config.pixel_data_handlers
-        pydicom.config.pixel_data_handlers = [numpy_handler, gdcm_handler]
-        yield
-        gdcm_handler.HAVE_GDCM_IN_MEMORY_SUPPORT = original_value
-        pydicom.config.pixel_data_handlers = original_handlers
 
+@pytest.fixture()
+def set_gdcm_max_buffer_size_25k():
+    original = gdcm_handler._GDCM_MAX_BUFFER_SIZE
+    gdcm_handler._GDCM_MAX_BUFFER_SIZE = 25000
+    yield
+
+    gdcm_handler._GDCM_MAX_BUFFER_SIZE = original
+
+
+@pytest.mark.skipif(not HAVE_GDCM, reason="GDCM not available")
+class TestsWithGDCM:
     @pytest.fixture(scope="class")
     def unicode_filename(self):
         unicode_filename = os.path.join(tempfile.gettempdir(), "ДИКОМ.dcm")
@@ -571,7 +567,54 @@ def test_pixel_rep_mismatch(self):
             arr[328:338, 106].tolist()
         )
 
+    def test_too_large_raises(self, set_gdcm_max_buffer_size_10k):
+        """Test exception raised if the frame is too large for GDCM to decode."""
+        # Single frame is 8192 bytes with 64 x 64 x 2
+        ds = dcmread(jpeg2000_lossless_name)
+        ds.Rows = 100
+        ds.pixel_array_options(use_v2_backend=True, decoding_plugin="gdcm")
+        msg = (
+            "GDCM cannot decode the pixel data as each frame will be larger than "
+            "GDCM's maximum buffer size"
+        )
+        with pytest.raises(ValueError, match=msg):
+            ds.pixel_array
+
+    def test_multi_frame_too_large_single_frame(self, set_gdcm_max_buffer_size_10k):
+        """Test decoding a multi-frame image where the total pixel data is too large
+        for GDCM to decode but each individual frame is just small enough to decode
+        separately.
+        """
+        # Single frame is 8192 bytes with 64 x 64 x 2
+        ds = dcmread(jpeg2000_lossless_name)
+        ds.NumberOfFrames = 2
+        frame = get_frame(ds.PixelData, 0, number_of_frames=1)
+        ds.PixelData = encapsulate([frame, frame])
+        ds.pixel_array_options(use_v2_backend=True, decoding_plugin="gdcm")
+        arr = ds.pixel_array
+
+        assert arr.shape == (2, 64, 64)
+        assert numpy.array_equal(arr[0], arr[1])
+
+    def test_multi_frame_too_large_multi_frame(self, set_gdcm_max_buffer_size_25k):
+        """Test decoding a multi-frame image where the total pixel data is too large
+        for GDCM to decode but multiple individual frames are small enough that many
+        frames can be decoded in one go.
+        """
+        ds = dcmread(jpeg2000_lossless_name)
+        # 11 frames will be decoded as (3, 3, 3, 2)
+        ds.NumberOfFrames = 11
+        frame = get_frame(ds.PixelData, 0, number_of_frames=1)
+        ds.PixelData = encapsulate([frame] * 11)
+        ds.pixel_array_options(use_v2_backend=True, decoding_plugin="gdcm")
+        arr = ds.pixel_array
 
+        assert arr.shape == (11, 64, 64)
+        for idx in range(11):
+            assert numpy.array_equal(arr[idx], arr[1])
+
+
+@pytest.mark.skipif(not HAVE_GDCM, reason="GDCM not available")
 class TestSupportFunctions:
     @pytest.fixture(scope="class")
     def dataset_2d(self):
@@ -585,16 +628,6 @@ def dataset_2d_compressed(self):
     def dataset_3d(self):
         return dcmread(color_3d_jpeg_baseline)
 
-    @pytest.mark.skipif(not HAVE_GDCM_IN_MEMORY_SUPPORT, reason=gdcm_im_missing_message)
-    def test_create_data_element_from_uncompressed_2d_dataset(self, dataset_2d):
-        data_element = gdcm_handler.create_data_element(dataset_2d)
-
-        assert 0x7FE0 == data_element.GetTag().GetGroup()
-        assert 0x0010 == data_element.GetTag().GetElement()
-        assert data_element.GetSequenceOfFragments() is None
-        assert data_element.GetByteValue() is not None
-
-    @pytest.mark.skipif(not HAVE_GDCM_IN_MEMORY_SUPPORT, reason=gdcm_im_missing_message)
     def test_create_data_element_from_compressed_2d_dataset(
         self, dataset_2d_compressed
     ):
@@ -605,7 +638,6 @@ def test_create_data_element_from_compressed_2d_dataset(
         assert data_element.GetSequenceOfFragments() is not None
         assert data_element.GetByteValue() is None
 
-    @pytest.mark.skipif(not HAVE_GDCM_IN_MEMORY_SUPPORT, reason=gdcm_im_missing_message)
     def test_create_data_element_from_3d_dataset(self, dataset_3d):
         data_element = gdcm_handler.create_data_element(dataset_3d)
 
@@ -614,31 +646,9 @@ def test_create_data_element_from_3d_dataset(self, dataset_3d):
         assert data_element.GetSequenceOfFragments() is not None
         assert data_element.GetByteValue() is None
 
-    @pytest.mark.skipif(not HAVE_GDCM_IN_MEMORY_SUPPORT, reason=gdcm_im_missing_message)
-    def test_create_image_from_2d_dataset(self, dataset_2d):
-        data_element = gdcm_handler.create_data_element(dataset_2d)
-        image = gdcm_handler.create_image(dataset_2d, data_element)
-        assert 2 == image.GetNumberOfDimensions()
-        assert [dataset_2d.Rows, dataset_2d.Columns] == image.GetDimensions()
-        pi_type = gdcm.PhotometricInterpretation.GetPIType(
-            dataset_2d.PhotometricInterpretation
-        )
-        assert pi_type == image.GetPhotometricInterpretation().GetType()
-
-        uid = str.__str__(dataset_2d.file_meta.TransferSyntaxUID)
-        assert uid == image.GetTransferSyntax().GetString()
-        pixel_format = image.GetPixelFormat()
-        assert dataset_2d.SamplesPerPixel == pixel_format.GetSamplesPerPixel()
-        assert dataset_2d.BitsAllocated == pixel_format.GetBitsAllocated()
-        assert dataset_2d.BitsStored == pixel_format.GetBitsStored()
-        assert dataset_2d.HighBit == pixel_format.GetHighBit()
-        px_repr = dataset_2d.PixelRepresentation
-        assert px_repr == pixel_format.GetPixelRepresentation()
-
-    @pytest.mark.skipif(not HAVE_GDCM_IN_MEMORY_SUPPORT, reason=gdcm_im_missing_message)
     def test_create_image_from_3d_dataset(self, dataset_3d):
         data_element = gdcm_handler.create_data_element(dataset_3d)
-        image = gdcm_handler.create_image(dataset_3d, data_element)
+        image = gdcm_handler.create_image(dataset_3d)
         assert 3 == image.GetNumberOfDimensions()
         assert [
             dataset_3d.Columns,