Repository: pydicom/pydicom

Issue:
Decompress data using GDCM without filename requirement
I've been in touch with the GDCM author Mathieu Malaterre to see how we can use GDCM without requiring the filename to be available as is currently the case (see #232).

He pointed me to this example:

http://gdcm.sourceforge.net/2.6/html/DecompressJPEGFile_8cs-example.xhtml

I'll be submitting a PR for this soon.


Discussion:
pep8speaks: Hello @erikced! Thanks for submitting the PR.

 - There are no PEP8 issues in the file [`pydicom/pixel_data_handlers/gdcm_handler.py`](https://github.com/erikced/pydicom/blob/e352c46771e28ee060aefd569c16bc209b9594d1/pydicom/pixel_data_handlers/gdcm_handler.py) !


scaramallion: It's a good proposal but it breaks support for released GDCM versions, which will be what most of our GDCM-using users use (hah), and it fails our unit tests. Can you modify the PR to support both the `Image` and `ImageReader` usages?

I'd also prefer unit test coverage (and docstrings) for the functions you split out since it'll save me some work later when I get to refactoring the testing for the GDCM handler.

There's also a `get_expected_length` function in the numpy handler that would be nice if it were used here (since it already has good test coverage). If you could move the function to `pixel_data_handlers.util` that'd be great since I was intending to do it myself in the next refactor.
erikced: Sure, it doesn't seem like too much work. Supporting both `ImageReader` and `Image` directly seems like a good way to support current and new GDCM releases (I never expected this to be merged due to the pre-release GDCM requirement) and it shold be relatively easy as the readout is identical.
scaramallion: Can you outline how you're compiling the current GDCM master for use with python? I'd be nice to add a build to Travis so your changes can be tested.
erikced: > Can you outline how you're compiling the current GDCM master for use with python? I'd be nice to add a build to Travis so your changes can be tested.

Hah, gdcm 2.8.8 which contains the required patch was just (as in between your comment and now) released. Building requires `cmake` and `swig`, to build and install:
* Download and extract https://github.com/malaterre/GDCM/archive/v2.8.8.tar.gz
* Create an out-of-source build directory and enter it
* Configure & build:  `cmake -DGDCM_WRAP_PYTHON:BOOL=ON -DGDCM_BUILD_DOCBOOK_MANPAGES:BOOL=OFF -DCMAKE_POSITION_INDEPENDENT_CODE:BOOL=ON  /path/to/gdcm && make`
* Copy relevant artifacts: `cp bin/{gdcm.py,gdcmswig.py,_gdcmswig.so} /path/to/python/site-packages/`

Position independent code may or may not be required, to direct `cmake` to the correct python headers and libraries it may be required to add something similar to `-DPYTHON_INCLUDE_DIR:FILEPATH=~/.pyenv/versions/3.6.2/include/python3.6m` `-DPYTHON_LIBRARY:FILEPATH=~/.pyenv/versions/3.6.2/lib/libpython3.6m.so` to the `cmake` step.
scaramallion: It would be nice. I'll see if I can get a Travis build going and fix that conflict 
scaramallion: Actually it looks like 2.8.8 is available on conda now anyway.
scaramallion: Codecov is having issues but looking at the report the coverage should increase.

@darcymason @mrbean-bremen anything else?

Patch:
diff --git a/.travis.yml b/.travis.yml
index b9f98b9db..2c6b33f7d 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -43,6 +43,7 @@ matrix:
     - env: DISTRIB="conda" PYTHON_VERSION="3.6" NUMPY=true  PILLOW=both JPEG_LS=false GDCM=false
     - env: DISTRIB="conda" PYTHON_VERSION="3.6" NUMPY=true  PILLOW=both JPEG_LS=true  GDCM=false
     - env: DISTRIB="conda" PYTHON_VERSION="3.6" NUMPY=true  PILLOW=both JPEG_LS=true  GDCM=true
+    - env: DISTRIB="conda" PYTHON_VERSION="3.6" NUMPY=true  PILLOW=false JPEG_LS=false GDCM=old
 
     - env: DISTRIB="conda" PYTHON_VERSION="3.7" NUMPY=false PILLOW=false JPEG_LS=false GDCM=false
     - env: DISTRIB="conda" PYTHON_VERSION="3.7" NUMPY=true  PILLOW=false JPEG_LS=false GDCM=false
diff --git a/build_tools/travis/install.sh b/build_tools/travis/install.sh
index cb52b4aaf..1e2b19ea2 100755
--- a/build_tools/travis/install.sh
+++ b/build_tools/travis/install.sh
@@ -59,6 +59,8 @@ if [[ "$DISTRIB" == "conda" ]]; then
     fi
     if [[ "$GDCM" == "true" ]]; then
         conda install --yes -c conda-forge gdcm
+    elif [[ "$GDCM" == "old" ]]; then
+        conda install --yes -c conda-forge gdcm=2.8.4
     fi
     # Install nose-timer via pip
     pip install nose-timer codecov
diff --git a/pydicom/pixel_data_handlers/gdcm_handler.py b/pydicom/pixel_data_handlers/gdcm_handler.py
index 86aa6badf..45dd3a945 100644
--- a/pydicom/pixel_data_handlers/gdcm_handler.py
+++ b/pydicom/pixel_data_handlers/gdcm_handler.py
@@ -12,11 +12,15 @@ except ImportError:
 try:
     import gdcm
     HAVE_GDCM = True
+    HAVE_GDCM_IN_MEMORY_SUPPORT = hasattr(gdcm.DataElement,
+                                          'SetByteStringValue')
 except ImportError:
     HAVE_GDCM = False
+    HAVE_GDCM_IN_MEMORY_SUPPORT = False
 
 import pydicom.uid
 from pydicom import compat
+from pydicom.pixel_data_handlers.util import get_expected_length, pixel_dtype
 
 
 HANDLER_NAME = 'GDCM'
@@ -73,6 +77,101 @@ def supports_transfer_syntax(transfer_syntax):
     return transfer_syntax in SUPPORTED_TRANSFER_SYNTAXES
 
 
+def create_data_element(dicom_dataset):
+    """Create a gdcm.DataElement containing PixelData from a FileDataset
+
+    Parameters
+    ----------
+    dicom_dataset : FileDataset
+
+
+    Returns
+    -------
+    gdcm.DataElement
+        Converted PixelData element
+    """
+    data_element = gdcm.DataElement(gdcm.Tag(0x7fe0, 0x0010))
+    if dicom_dataset.file_meta.TransferSyntaxUID.is_compressed:
+        if getattr(dicom_dataset, 'NumberOfFrames', 1) > 1:
+            pixel_data_sequence = pydicom.encaps.decode_data_sequence(
+                dicom_dataset.PixelData)
+        else:
+            pixel_data_sequence = [
+                pydicom.encaps.defragment_data(dicom_dataset.PixelData)
+            ]
+
+        fragments = gdcm.SequenceOfFragments.New()
+        for pixel_data in pixel_data_sequence:
+            fragment = gdcm.Fragment()
+            fragment.SetByteStringValue(pixel_data)
+            fragments.AddFragment(fragment)
+        data_element.SetValue(fragments.__ref__())
+    else:
+        data_element.SetByteStringValue(dicom_dataset.PixelData)
+
+    return data_element
+
+
+def create_image(dicom_dataset, data_element):
+    """Create a gdcm.Image from a FileDataset and a gdcm.DataElement containing
+    PixelData (0x7fe0, 0x0010)
+
+    Parameters
+    ----------
+    dicom_dataset : FileDataset
+    data_element : gdcm.DataElement
+        DataElement containing PixelData
+
+    Returns
+    -------
+    gdcm.Image
+    """
+    image = gdcm.Image()
+    number_of_frames = getattr(dicom_dataset, 'NumberOfFrames', 1)
+    image.SetNumberOfDimensions(2 if number_of_frames == 1 else 3)
+    image.SetDimensions(
+        (dicom_dataset.Columns, dicom_dataset.Rows, number_of_frames))
+    image.SetDataElement(data_element)
+    pi_type = gdcm.PhotometricInterpretation.GetPIType(
+        dicom_dataset.PhotometricInterpretation)
+    image.SetPhotometricInterpretation(
+        gdcm.PhotometricInterpretation(pi_type))
+    ts_type = gdcm.TransferSyntax.GetTSType(
+        str.__str__(dicom_dataset.file_meta.TransferSyntaxUID))
+    image.SetTransferSyntax(gdcm.TransferSyntax(ts_type))
+    pixel_format = gdcm.PixelFormat(
+        dicom_dataset.SamplesPerPixel, dicom_dataset.BitsAllocated,
+        dicom_dataset.BitsStored, dicom_dataset.HighBit,
+        dicom_dataset.PixelRepresentation)
+    image.SetPixelFormat(pixel_format)
+    if 'PlanarConfiguration' in dicom_dataset:
+        image.SetPlanarConfiguration(dicom_dataset.PlanarConfiguration)
+    return image
+
+
+def create_image_reader(filename):
+    """Create a gdcm.ImageReader
+
+    Parameters
+    ----------
+    filename: str or unicode (Python 2)
+
+    Returns
+    -------
+    gdcm.ImageReader
+    """
+    image_reader = gdcm.ImageReader()
+    if compat.in_py2:
+        if isinstance(filename, unicode):
+            image_reader.SetFileName(
+                filename.encode(sys.getfilesystemencoding()))
+        else:
+            image_reader.SetFileName(filename)
+    else:
+        image_reader.SetFileName(filename)
+    return image_reader
+
+
 def get_pixeldata(dicom_dataset):
     """
     Use the GDCM package to decode the PixelData attribute
@@ -97,48 +196,19 @@ def get_pixeldata(dicom_dataset):
         if the decoded amount of data does not match the expected amount
     """
 
-    # read the file using GDCM
-    # FIXME this should just use dicom_dataset.PixelData
-    # instead of dicom_dataset.filename
-    #       but it is unclear how this should be achieved using GDCM
     if not HAVE_GDCM:
         msg = ("GDCM requires both the gdcm package and numpy "
                "and one or more could not be imported")
         raise ImportError(msg)
 
-    gdcm_image_reader = gdcm.ImageReader()
-    if compat.in_py2:
-        if isinstance(dicom_dataset.filename, unicode):
-            gdcm_image_reader.SetFileName(
-                dicom_dataset.filename.encode(sys.getfilesystemencoding()))
-        else:
-            gdcm_image_reader.SetFileName(dicom_dataset.filename)
+    if HAVE_GDCM_IN_MEMORY_SUPPORT:
+        gdcm_data_element = create_data_element(dicom_dataset)
+        gdcm_image = create_image(dicom_dataset, gdcm_data_element)
     else:
-        # python 3
-        gdcm_image_reader.SetFileName(dicom_dataset.filename)
-
-    if not gdcm_image_reader.Read():
-        raise TypeError("GDCM could not read DICOM image")
-
-    gdcm_image = gdcm_image_reader.GetImage()
-
-    # determine the correct numpy datatype
-    gdcm_numpy_typemap = {
-        gdcm.PixelFormat.INT8:     numpy.int8,
-        gdcm.PixelFormat.UINT8:    numpy.uint8,
-        gdcm.PixelFormat.UINT16:   numpy.uint16,
-        gdcm.PixelFormat.INT16:    numpy.int16,
-        gdcm.PixelFormat.UINT32:   numpy.uint32,
-        gdcm.PixelFormat.INT32:    numpy.int32,
-        gdcm.PixelFormat.FLOAT32:  numpy.float32,
-        gdcm.PixelFormat.FLOAT64:  numpy.float64
-    }
-    gdcm_pixel_format = gdcm_image.GetPixelFormat().GetScalarType()
-    if gdcm_pixel_format in gdcm_numpy_typemap:
-        numpy_dtype = gdcm_numpy_typemap[gdcm_pixel_format]
-    else:
-        raise TypeError('{0} is not a GDCM supported '
-                        'pixel format'.format(gdcm_pixel_format))
+        gdcm_image_reader = create_image_reader(dicom_dataset.filename)
+        if not gdcm_image_reader.Read():
+            raise TypeError("GDCM could not read DICOM image")
+        gdcm_image = gdcm_image_reader.GetImage()
 
     # GDCM returns char* as type str. Under Python 2 `str` are
     # byte arrays by default. Python 3 decodes this to
@@ -148,57 +218,34 @@ def get_pixeldata(dicom_dataset):
     # error handler configured.
     # Therefore, we can encode them back to their original bytearray
     # representation on Python 3 by using the same parameters.
-    pixel_bytearray = gdcm_image.GetBuffer()
-    if sys.version_info >= (3, 0):
-        pixel_bytearray = pixel_bytearray.encode("utf-8",
-                                                 "surrogateescape")
-
-    # if GDCM indicates that a byte swap is in order, make
-    #   sure to inform numpy as well
-    if gdcm_image.GetNeedByteSwap():
-        numpy_dtype = numpy_dtype.newbyteorder('S')
+    if compat.in_py2:
+        pixel_bytearray = gdcm_image.GetBuffer()
+    else:
+        pixel_bytearray = gdcm_image.GetBuffer().encode(
+            "utf-8", "surrogateescape")
 
     # 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.
-
-    n_bytes = (dicom_dataset.Rows *
-               dicom_dataset.Columns *
-               numpy.dtype(numpy_dtype).itemsize)
-    try:
-        n_bytes *= dicom_dataset.NumberOfFrames
-    except Exception:
-        pass
-    try:
-        n_bytes *= dicom_dataset.SamplesPerPixel
-    except Exception:
-        pass
-
-    if len(pixel_bytearray) > n_bytes:
+    expected_length_bytes = get_expected_length(dicom_dataset)
+    if len(pixel_bytearray) > expected_length_bytes:
         # We make sure that all the bytes after are in fact zeros
-        padding = pixel_bytearray[n_bytes:]
+        padding = pixel_bytearray[expected_length_bytes:]
         if numpy.any(numpy.frombuffer(padding, numpy.byte)):
-            pixel_bytearray = pixel_bytearray[:n_bytes]
+            pixel_bytearray = pixel_bytearray[:expected_length_bytes]
         else:
             # We revert to the old behavior which should then result
             #   in a Numpy error later on.
             pass
 
+    numpy_dtype = pixel_dtype(dicom_dataset)
     pixel_array = numpy.frombuffer(pixel_bytearray, dtype=numpy_dtype)
 
-    length_of_pixel_array = pixel_array.nbytes
-    expected_length = dicom_dataset.Rows * dicom_dataset.Columns
-
-    expected_length *= dicom_dataset.get("NumberOfFrames", 1)
-    expected_length *= dicom_dataset.get("SamplesPerPixel", 1)
-
-    if dicom_dataset.BitsAllocated > 8:
-        expected_length *= (dicom_dataset.BitsAllocated // 8)
-
-    if length_of_pixel_array != expected_length:
+    expected_length_pixels = get_expected_length(dicom_dataset, 'pixels')
+    if pixel_array.size != expected_length_pixels:
         raise AttributeError("Amount of pixel data %d does "
                              "not match the expected data %d" %
-                             (length_of_pixel_array, expected_length))
+                             (pixel_array.size, expected_length_pixels))
 
     if should_change_PhotometricInterpretation_to_RGB(dicom_dataset):
         dicom_dataset.PhotometricInterpretation = "RGB"
diff --git a/pydicom/pixel_data_handlers/numpy_handler.py b/pydicom/pixel_data_handlers/numpy_handler.py
index f3030abe7..2d5c8a347 100644
--- a/pydicom/pixel_data_handlers/numpy_handler.py
+++ b/pydicom/pixel_data_handlers/numpy_handler.py
@@ -42,7 +42,7 @@ try:
 except ImportError:
     HAVE_NP = False
 
-from pydicom.pixel_data_handlers.util import pixel_dtype
+from pydicom.pixel_data_handlers.util import pixel_dtype, get_expected_length
 import pydicom.uid
 
 HANDLER_NAME = 'Numpy'
@@ -92,60 +92,6 @@ def should_change_PhotometricInterpretation_to_RGB(ds):
     return False
 
 
-def get_expected_length(ds, unit='bytes'):
-    """Return the expected length (in bytes or pixels) of the pixel data.
-
-    +-----------------------------------+------+-------------+
-    | Element                           | Type | Required or |
-    +-------------+---------------------+      | optional    |
-    | Tag         | Keyword             |      |             |
-    +=============+=====================+======+=============+
-    | (0028,0002) | SamplesPerPixel     | 1    | Required    |
-    +-------------+---------------------+------+-------------+
-    | (0028,0008) | NumberOfFrames      | 1C   | Optional    |
-    +-------------+---------------------+------+-------------+
-    | (0028,0010) | Rows                | 1    | Required    |
-    +-------------+---------------------+------+-------------+
-    | (0028,0011) | Columns             | 1    | Required    |
-    +-------------+---------------------+------+-------------+
-    | (0028,0100) | BitsAllocated       | 1    | Required    |
-    +-------------+---------------------+------+-------------+
-
-    Parameters
-    ----------
-    ds : dataset.Dataset
-        The DICOM dataset containing the Image Pixel module and pixel data.
-    unit : str, optional
-        If 'bytes' then returns the expected length of the Pixel Data in
-        whole bytes and NOT including an odd length trailing NULL padding
-        byte. If 'pixels' then returns the expected length of the Pixel Data
-        in terms of the total number of pixels (default 'bytes').
-
-    Returns
-    -------
-    int
-        The expected length of the pixel data in either whole bytes or pixels,
-        excluding the NULL trailing padding byte for odd length data.
-    """
-    length = ds.Rows * ds.Columns * ds.SamplesPerPixel
-    length *= getattr(ds, 'NumberOfFrames', 1)
-
-    if unit == 'pixels':
-        return length
-
-    # Correct for the number of bytes per pixel
-    bits_allocated = ds.BitsAllocated
-    if bits_allocated == 1:
-        # Determine the nearest whole number of bytes needed to contain
-        #   1-bit pixel data. e.g. 10 x 10 1-bit pixels is 100 bits, which
-        #   are packed into 12.5 -> 13 bytes
-        length = length // 8 + (length % 8 > 0)
-    else:
-        length *= bits_allocated // 8
-
-    return length
-
-
 def pack_bits(arr):
     """Pack a binary numpy ndarray into bytes for use with Pixel Data.
 
diff --git a/pydicom/pixel_data_handlers/util.py b/pydicom/pixel_data_handlers/util.py
index d127dc66e..87dd6c436 100644
--- a/pydicom/pixel_data_handlers/util.py
+++ b/pydicom/pixel_data_handlers/util.py
@@ -323,6 +323,60 @@ def reshape_pixel_array(ds, arr):
     return arr
 
 
+def get_expected_length(ds, unit='bytes'):
+    """Return the expected length (in bytes or pixels) of the pixel data.
+
+    +-----------------------------------+------+-------------+
+    | Element                           | Type | Required or |
+    +-------------+---------------------+      | optional    |
+    | Tag         | Keyword             |      |             |
+    +=============+=====================+======+=============+
+    | (0028,0002) | SamplesPerPixel     | 1    | Required    |
+    +-------------+---------------------+------+-------------+
+    | (0028,0008) | NumberOfFrames      | 1C   | Optional    |
+    +-------------+---------------------+------+-------------+
+    | (0028,0010) | Rows                | 1    | Required    |
+    +-------------+---------------------+------+-------------+
+    | (0028,0011) | Columns             | 1    | Required    |
+    +-------------+---------------------+------+-------------+
+    | (0028,0100) | BitsAllocated       | 1    | Required    |
+    +-------------+---------------------+------+-------------+
+
+    Parameters
+    ----------
+    ds : dataset.Dataset
+        The DICOM dataset containing the Image Pixel module and pixel data.
+    unit : str, optional
+        If 'bytes' then returns the expected length of the Pixel Data in
+        whole bytes and NOT including an odd length trailing NULL padding
+        byte. If 'pixels' then returns the expected length of the Pixel Data
+        in terms of the total number of pixels (default 'bytes').
+
+    Returns
+    -------
+    int
+        The expected length of the pixel data in either whole bytes or pixels,
+        excluding the NULL trailing padding byte for odd length data.
+    """
+    length = ds.Rows * ds.Columns * ds.SamplesPerPixel
+    length *= getattr(ds, 'NumberOfFrames', 1)
+
+    if unit == 'pixels':
+        return length
+
+    # Correct for the number of bytes per pixel
+    bits_allocated = ds.BitsAllocated
+    if bits_allocated == 1:
+        # Determine the nearest whole number of bytes needed to contain
+        #   1-bit pixel data. e.g. 10 x 10 1-bit pixels is 100 bits, which
+        #   are packed into 12.5 -> 13 bytes
+        length = length // 8 + (length % 8 > 0)
+    else:
+        length *= bits_allocated // 8
+
+    return length
+
+
 def _convert_RGB_to_YBR_FULL(arr):
     """Return an ndarray converted from RGB to YBR_FULL color space.
 


Test patch:
diff --git a/pydicom/tests/test_environment.py b/pydicom/tests/test_environment.py
index 82c70c36b..ab1e861ea 100644
--- a/pydicom/tests/test_environment.py
+++ b/pydicom/tests/test_environment.py
@@ -7,7 +7,7 @@ The current pydicom testing environments are as follows:
   * Python 2.7:
     * no additional packages
     * numpy
-    * numpy, gdcm
+    * numpy, gdcm (newest and v2.8.4)
     * numpy, pillow (jpg, jpg2k)
     * numpy, jpeg-ls
     * numpy, pillow (jpg, jpg2k), jpeg-ls
@@ -32,7 +32,7 @@ PYTHON_VERSION: 2.7, 3.4, 3.5, 3.6, 3.7
 NUMPY: true, false
 PILLOW: jpeg, both, false
 JPEG_LS: false, true
-GDCM: false, true
+GDCM: false, true, old
 """
 import os
 import platform
@@ -174,6 +174,12 @@ class TestBuilds(object):
         elif have_gdcm == 'false':
             with pytest.raises(ImportError):
                 import gdcm
+        elif have_gdcm == 'old':
+            try:
+                import gdcm
+            except ImportError:
+                pytest.fail("GDCM is 'old' but gdcm is not importable")
+            assert gdcm.Version_GetVersion() == '2.8.4'
         else:
             raise NotImplementedError(
                 "Unknown 'GDCM' value of '{}'".format(have_gdcm)
diff --git a/pydicom/tests/test_gdcm_pixel_data.py b/pydicom/tests/test_gdcm_pixel_data.py
index 4d4c41fda..5a4b9d9e5 100644
--- a/pydicom/tests/test_gdcm_pixel_data.py
+++ b/pydicom/tests/test_gdcm_pixel_data.py
@@ -14,6 +14,8 @@ from pydicom.pixel_data_handlers.util import _convert_YBR_FULL_to_RGB
 from pydicom.tag import Tag
 from pydicom import compat
 gdcm_missing_message = "GDCM is not available in this test environment"
+gdcm_im_missing_message = "GDCM is not available or in-memory decoding"\
+    " not supported with this GDCM version"
 gdcm_present_message = "GDCM is being tested"
 have_numpy_testing = True
 try:
@@ -31,8 +33,12 @@ except AttributeError:
 try:
     import pydicom.pixel_data_handlers.gdcm_handler as gdcm_handler
     HAVE_GDCM = gdcm_handler.HAVE_GDCM
+    HAVE_GDCM_IN_MEMORY_SUPPORT = gdcm_handler.HAVE_GDCM_IN_MEMORY_SUPPORT
+    if HAVE_GDCM:
+        import gdcm
 except ImportError as e:
     HAVE_GDCM = False
+    HAVE_GDCM_IN_MEMORY_SUPPORT = False
     gdcm_handler = None
 
 try:
@@ -57,6 +63,8 @@ jpeg_ls_lossless_name = get_testdata_files(
     "MR_small_jpeg_ls_lossless.dcm")[0]
 jpeg_lossy_name = get_testdata_files("JPEG-lossy.dcm")[0]
 jpeg_lossless_name = get_testdata_files("JPEG-LL.dcm")[0]
+jpeg_lossless_odd_data_size_name = get_testdata_files(
+    'SC_rgb_small_odd_jpeg.dcm')[0]
 deflate_name = get_testdata_files("image_dfl.dcm")[0]
 rtstruct_name = get_testdata_files("rtstruct.dcm")[0]
 priv_SQ_name = get_testdata_files("priv_SQ.dcm")[0]
@@ -238,192 +246,8 @@ class GDCM_JPEGlosslessTests_no_gdcm(unittest.TestCase):
             _ = self.jpeg_lossless.pixel_array
 
 
-@pytest.mark.skipif(not HAVE_GDCM, reason=gdcm_missing_message)
-class GDCM_JPEG_LS_Tests_with_gdcm(unittest.TestCase):
-    def setUp(self):
-        if compat.in_py2:
-            self.utf8_filename = os.path.join(
-                tempfile.gettempdir(), "ДИКОМ.dcm")
-            self.unicode_filename = self.utf8_filename.decode("utf8")
-            shutil.copyfile(jpeg_ls_lossless_name.decode("utf8"),
-                            self.unicode_filename)
-        else:
-            self.unicode_filename = os.path.join(
-                tempfile.gettempdir(), "ДИКОМ.dcm")
-            shutil.copyfile(jpeg_ls_lossless_name, self.unicode_filename)
-        self.jpeg_ls_lossless = dcmread(self.unicode_filename)
-        self.mr_small = dcmread(mr_name)
-        self.emri_jpeg_ls_lossless = dcmread(emri_jpeg_ls_lossless)
-        self.emri_small = dcmread(emri_name)
-        self.original_handlers = pydicom.config.pixel_data_handlers
-        pydicom.config.pixel_data_handlers = [numpy_handler, gdcm_handler]
-
-    def tearDown(self):
-        pydicom.config.pixel_data_handlers = self.original_handlers
-        os.remove(self.unicode_filename)
-
-    def test_JPEG_LS_PixelArray(self):
-        a = self.jpeg_ls_lossless.pixel_array
-        b = self.mr_small.pixel_array
-        self.assertEqual(
-            a.mean(),
-            b.mean(),
-            "using GDCM Decoded pixel data is not "
-            "all {0} (mean == {1})".format(b.mean(), a.mean()))
-
-        assert a.flags.writeable
-
-    def test_emri_JPEG_LS_PixelArray_with_gdcm(self):
-        a = self.emri_jpeg_ls_lossless.pixel_array
-        b = self.emri_small.pixel_array
-        self.assertEqual(
-            a.mean(),
-            b.mean(),
-            "Decoded pixel data is not all {0} "
-            "(mean == {1})".format(b.mean(), a.mean()))
-
-        assert a.flags.writeable
-
-
-@pytest.mark.skipif(not HAVE_GDCM, reason=gdcm_missing_message)
-class GDCM_JPEG2000Tests_with_gdcm(unittest.TestCase):
-    def setUp(self):
-        self.jpeg_2k = dcmread(jpeg2000_name)
-        self.jpeg_2k_lossless = dcmread(jpeg2000_lossless_name)
-        self.mr_small = dcmread(mr_name)
-        self.emri_jpeg_2k_lossless = dcmread(emri_jpeg_2k_lossless)
-        self.emri_small = dcmread(emri_name)
-        self.sc_rgb_jpeg2k_gdcm_KY = dcmread(sc_rgb_jpeg2k_gdcm_KY)
-        self.ground_truth_sc_rgb_jpeg2k_gdcm_KY_gdcm = dcmread(
-            ground_truth_sc_rgb_jpeg2k_gdcm_KY_gdcm)
-        self.original_handlers = pydicom.config.pixel_data_handlers
-        pydicom.config.pixel_data_handlers = [numpy_handler, gdcm_handler]
-
-    def tearDown(self):
-        pydicom.config.pixel_data_handlers = self.original_handlers
-
-    def test_JPEG2000(self):
-        """JPEG2000: Returns correct values for sample data elements"""
-        # XX also tests multiple-valued AT data element
-        expected = [Tag(0x0054, 0x0010), Tag(0x0054, 0x0020)]
-        got = self.jpeg_2k.FrameIncrementPointer
-        self.assertEqual(
-            got,
-            expected,
-            "JPEG2000 file, Frame Increment Pointer: "
-            "expected %s, got %s" % (expected, got))
-
-        got = self.jpeg_2k.DerivationCodeSequence[0].CodeMeaning
-        expected = 'Lossy Compression'
-        self.assertEqual(
-            got,
-            expected,
-            "JPEG200 file, Code Meaning got %s, "
-            "expected %s" % (got, expected))
-
-    def test_JPEG2000PixelArray(self):
-        a = self.jpeg_2k_lossless.pixel_array
-        b = self.mr_small.pixel_array
-        self.assertEqual(
-            a.mean(),
-            b.mean(),
-            "Decoded pixel data is not all {0} "
-            "(mean == {1})".format(b.mean(), a.mean()))
-
-        assert a.flags.writeable
-
-    def test_emri_JPEG2000PixelArray(self):
-        a = self.emri_jpeg_2k_lossless.pixel_array
-        b = self.emri_small.pixel_array
-        self.assertEqual(
-            a.mean(),
-            b.mean(),
-            "Decoded pixel data is not all {0} "
-            "(mean == {1})".format(b.mean(), a.mean()))
-
-        assert a.flags.writeable
-
-    def test_jpeg2000_lossy(self):
-        a = self.sc_rgb_jpeg2k_gdcm_KY.pixel_array
-        b = self.ground_truth_sc_rgb_jpeg2k_gdcm_KY_gdcm.pixel_array
-        if have_numpy_testing:
-            numpy.testing.assert_array_equal(a, b)
-        else:
-            self.assertEqual(
-                a.mean(),
-                b.mean(),
-                "Decoded pixel data is not all {0} "
-                "(mean == {1})".format(b.mean(), a.mean()))
-
-        assert a.flags.writeable
-
-
-@pytest.mark.skipif(not HAVE_GDCM, reason=gdcm_missing_message)
-class GDCM_JPEGlossyTests_with_gdcm(unittest.TestCase):
-
-    def setUp(self):
-        self.jpeg_lossy = dcmread(jpeg_lossy_name)
-        self.color_3d_jpeg = dcmread(color_3d_jpeg_baseline)
-        self.original_handlers = pydicom.config.pixel_data_handlers
-        pydicom.config.pixel_data_handlers = [numpy_handler, gdcm_handler]
-
-    def tearDown(self):
-        pydicom.config.pixel_data_handlers = self.original_handlers
-
-    def testJPEGlossless_odd_data_size(self):
-        test_file = get_testdata_files('SC_rgb_small_odd_jpeg.dcm')[0]
-        ds = dcmread(test_file)
-        pixel_data = ds.pixel_array
-        assert pixel_data.nbytes == 27
-        assert pixel_data.shape == (3, 3, 3)
-
-    def test_JPEGlossy(self):
-        """JPEG-lossy: Returns correct values for sample data elements"""
-        got = self.jpeg_lossy.DerivationCodeSequence[0].CodeMeaning
-        expected = 'Lossy Compression'
-        self.assertEqual(
-            got,
-            expected,
-            "JPEG-lossy file, Code Meaning got %s, "
-            "expected %s" % (got, expected))
-
-    def test_JPEGlossyPixelArray(self):
-        a = self.jpeg_lossy.pixel_array
-        self.assertEqual(a.shape, (1024, 256))
-        # this test points were manually identified in Osirix viewer
-        self.assertEqual(a[420, 140], 244)
-        self.assertEqual(a[230, 120], 95)
-
-        assert a.flags.writeable
-
-    def test_JPEGBaselineColor3DPixelArray(self):
-        self.assertEqual(
-            self.color_3d_jpeg.PhotometricInterpretation,
-            "YBR_FULL_422")
-        a = self.color_3d_jpeg.pixel_array
-
-        assert a.flags.writeable
-
-        self.assertEqual(a.shape, (120, 480, 640, 3))
-        a = _convert_YBR_FULL_to_RGB(a)
-        # this test points were manually identified in Osirix viewer
-        self.assertEqual(tuple(a[3, 159, 290, :]), (41, 41, 41))
-        self.assertEqual(tuple(a[3, 169, 290, :]), (57, 57, 57))
-        self.assertEqual(
-            self.color_3d_jpeg.PhotometricInterpretation,
-            "YBR_FULL_422")
-
-
-@pytest.fixture(scope="module")
-def test_with_gdcm():
-    original_handlers = pydicom.config.pixel_data_handlers
-    pydicom.config.pixel_data_handlers = [numpy_handler, gdcm_handler]
-    yield original_handlers
-    pydicom.config.pixel_data_handlers = original_handlers
-
-
 if have_pytest_param:
-    test_ids = [
+    pi_rgb_test_ids = [
         "JPEG_RGB_RGB",
         "JPEG_RGB_411_AS_YBR_FULL",
         "JPEG_RGB_411_AS_YBR_FULL_422",
@@ -432,7 +256,7 @@ if have_pytest_param:
         "JPEG_RGB_444_AS_YBR_FULL",
     ]
 
-    testdata = [
+    pi_rgb_testdata = [
         (sc_rgb_jpeg_dcmtk_RGB,
          "RGB",
          [
@@ -543,12 +367,18 @@ if have_pytest_param:
             marks=pytest.mark.xfail(
                 reason="GDCM does not support "
                 "non default jpeg lossy colorspaces"))]
+
+    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))]
 else:
     # python 3.4 can't parameterize with xfails...
-    test_ids = [
+    pi_rgb_test_ids = [
         "JPEG_RGB_RGB",
     ]
-    testdata = [
+    pi_rgb_testdata = [
         (sc_rgb_jpeg_dcmtk_RGB,
          "RGB",
          [
@@ -566,71 +396,331 @@ else:
          False),
     ]
 
-
-@pytest.mark.skipif(
-    not HAVE_GDCM,
-    reason=gdcm_missing_message)
-@pytest.mark.parametrize(
-    "image,PhotometricInterpretation,results,convert_yuv_to_rgb",
-    testdata,
-    ids=test_ids)
-def test_PI_RGB(test_with_gdcm,
-                image,
-                PhotometricInterpretation,
-                results,
-                convert_yuv_to_rgb):
-    t = dcmread(image)
-    assert t.PhotometricInterpretation == PhotometricInterpretation
-    a = t.pixel_array
-
-    assert a.flags.writeable
-
-    assert a.shape == (100, 100, 3)
-    if convert_yuv_to_rgb:
-        a = _convert_YBR_FULL_to_RGB(a)
-    # this test points are from the ImageComments tag
-    assert tuple(a[5, 50, :]) == results[0]
-    assert tuple(a[15, 50, :]) == results[1]
-    assert tuple(a[25, 50, :]) == results[2]
-    assert tuple(a[35, 50, :]) == results[3]
-    assert tuple(a[45, 50, :]) == results[4]
-    assert tuple(a[55, 50, :]) == results[5]
-    assert tuple(a[65, 50, :]) == results[6]
-    assert tuple(a[75, 50, :]) == results[7]
-    assert tuple(a[85, 50, :]) == results[8]
-    assert tuple(a[95, 50, :]) == results[9]
-    assert t.PhotometricInterpretation == PhotometricInterpretation
-
-
-@pytest.mark.skipif(not HAVE_GDCM, reason=gdcm_missing_message)
-class GDCM_JPEGlosslessTests_with_gdcm(unittest.TestCase):
-    def setUp(self):
-        self.jpeg_lossless = dcmread(jpeg_lossless_name)
-        self.original_handlers = pydicom.config.pixel_data_handlers
+    if HAVE_GDCM_IN_MEMORY_SUPPORT:
+        with_gdcm_params = ['File', 'InMemory']
+    elif HAVE_GDCM:
+        with_gdcm_params = ['File']
+    else:
+        with_gdcm_params = []
+
+
+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
 
-    def tearDown(self):
-        pydicom.config.pixel_data_handlers = self.original_handlers
+    @pytest.fixture(scope='class')
+    def unicode_filename(self):
+        if compat.in_py2:
+            utf8_filename = os.path.join(tempfile.gettempdir(), "ДИКОМ.dcm")
+            unicode_filename = utf8_filename.decode("utf8")
+            shutil.copyfile(jpeg_ls_lossless_name.decode("utf8"),
+                            unicode_filename)
+        else:
+            unicode_filename = os.path.join(
+                tempfile.gettempdir(), "ДИКОМ.dcm")
+            shutil.copyfile(jpeg_ls_lossless_name, unicode_filename)
+        yield unicode_filename
+        os.remove(unicode_filename)
 
-    def testJPEGlossless(self):
+    @pytest.fixture
+    def jpeg_ls_lossless(self, unicode_filename):
+        return dcmread(unicode_filename)
+
+    @pytest.fixture
+    def sc_rgb_jpeg2k_gdcm_KY(self):
+        return dcmread(sc_rgb_jpeg2k_gdcm_KY)
+
+    @pytest.fixture(scope='class')
+    def ground_truth_sc_rgb_jpeg2k_gdcm_KY_gdcm(self):
+        return dcmread(ground_truth_sc_rgb_jpeg2k_gdcm_KY_gdcm)
+
+    @pytest.fixture
+    def jpeg_2k(self):
+        return dcmread(jpeg2000_name)
+
+    @pytest.fixture
+    def jpeg_2k_lossless(self):
+        return dcmread(jpeg2000_lossless_name)
+
+    @pytest.fixture(scope='class')
+    def mr_small(self):
+        return dcmread(mr_name)
+
+    @pytest.fixture(scope='class')
+    def emri_small(self):
+        return dcmread(emri_name)
+
+    @pytest.fixture
+    def emri_jpeg_ls_lossless(self):
+        return dcmread(emri_jpeg_ls_lossless)
+
+    @pytest.fixture
+    def emri_jpeg_2k_lossless(self):
+        return dcmread(emri_jpeg_2k_lossless)
+
+    @pytest.fixture
+    def color_3d_jpeg(self):
+        return dcmread(color_3d_jpeg_baseline)
+
+    @pytest.fixture
+    def jpeg_lossy(self):
+        return dcmread(jpeg_lossy_name)
+
+    @pytest.fixture
+    def jpeg_lossless(self):
+        return dcmread(jpeg_lossless_name)
+
+    @pytest.fixture
+    def jpeg_lossless_odd_data_size(self):
+        return dcmread(jpeg_lossless_odd_data_size_name)
+
+    def test_JPEG_LS_PixelArray(self, jpeg_ls_lossless, mr_small):
+        a = jpeg_ls_lossless.pixel_array
+        b = mr_small.pixel_array
+        assert a.mean() == b.mean(), "using GDCM Decoded pixel data is not "\
+            "all {0} (mean == {1})".format(b.mean(), a.mean())
+
+        assert a.flags.writeable
+
+    def test_emri_JPEG_LS_PixelArray_with_gdcm(self, emri_jpeg_ls_lossless,
+                                               emri_small):
+        a = emri_jpeg_ls_lossless.pixel_array
+        b = emri_small.pixel_array
+        assert a.mean() == b.mean(), "Decoded pixel data is not all {0} "\
+            "(mean == {1})".format(b.mean(), a.mean())
+
+        assert a.flags.writeable
+
+    def test_JPEG2000(self, jpeg_2k):
+        """JPEG2000: Returns correct values for sample data elements"""
+        # XX also tests multiple-valued AT data element
+        expected = [Tag(0x0054, 0x0010), Tag(0x0054, 0x0020)]
+        got = jpeg_2k.FrameIncrementPointer
+        assert got == expected, "JPEG2000 file, Frame Increment Pointer: "\
+            "expected %s, got %s" % (expected, got)
+
+        got = jpeg_2k.DerivationCodeSequence[0].CodeMeaning
+        expected = 'Lossy Compression'
+        assert got == expected, "JPEG200 file, Code Meaning got %s, "\
+            "expected %s" % (got, expected)
+
+    def test_JPEG2000PixelArray(self, jpeg_2k_lossless, mr_small):
+        a = jpeg_2k_lossless.pixel_array
+        b = mr_small.pixel_array
+        assert a.mean() == b.mean(), "Decoded pixel data is not all {0} "\
+            "(mean == {1})".format(b.mean(), a.mean())
+
+        assert a.flags.writeable
+
+    def test_emri_JPEG2000PixelArray(self, emri_jpeg_2k_lossless, emri_small):
+        a = emri_jpeg_2k_lossless.pixel_array
+        b = emri_small.pixel_array
+        assert a.mean() == b.mean(), "Decoded pixel data is not all {0} "\
+            "(mean == {1})".format(b.mean(), a.mean())
+
+        assert a.flags.writeable
+
+    def test_JPEG2000_lossy(self, sc_rgb_jpeg2k_gdcm_KY,
+                            ground_truth_sc_rgb_jpeg2k_gdcm_KY_gdcm):
+        a = sc_rgb_jpeg2k_gdcm_KY.pixel_array
+        b = ground_truth_sc_rgb_jpeg2k_gdcm_KY_gdcm.pixel_array
+        if have_numpy_testing:
+            numpy.testing.assert_array_equal(a, b)
+        else:
+            assert a.mean() == b.mean(), "Decoded pixel data is not all {0} "\
+                "(mean == {1})".format(b.mean(), a.mean())
+
+        assert a.flags.writeable
+
+    def test_JPEGlossless(self, jpeg_lossless):
         """JPEGlossless: Returns correct values for sample data elements"""
-        got = self.\
-            jpeg_lossless.\
+        got = jpeg_lossless.\
             SourceImageSequence[0].\
             PurposeOfReferenceCodeSequence[0].CodeMeaning
         expected = 'Uncompressed predecessor'
-        self.assertEqual(
-            got,
-            expected,
-            "JPEG-lossless file, Code Meaning got %s, "
-            "expected %s" % (got, expected))
+        assert got == expected, "JPEG-lossless file, Code Meaning got %s, "\
+            "expected %s" % (got, expected)
 
-    def testJPEGlosslessPixelArray(self):
+    def test_JPEGlosslessPixelArray(self, jpeg_lossless):
         """JPEGlossless: Fails gracefully when uncompressed data asked for"""
-        a = self.jpeg_lossless.pixel_array
-        self.assertEqual(a.shape, (1024, 256))
+        a = jpeg_lossless.pixel_array
+        assert a.shape, (1024, 256)
+        # this test points were manually identified in Osirix viewer
+        assert a[420, 140], 227
+        assert a[230, 120], 105
+
+        assert a.flags.writeable
+
+    def test_JPEGlossless_odd_data_size(self, jpeg_lossless_odd_data_size):
+        pixel_data = jpeg_lossless_odd_data_size.pixel_array
+        assert pixel_data.nbytes == 27
+        assert pixel_data.shape == (3, 3, 3)
+
+    def test_JPEGlossy(self, jpeg_lossy):
+        """JPEG-lossy: Returns correct values for sample data elements"""
+        got = jpeg_lossy.DerivationCodeSequence[0].CodeMeaning
+        expected = 'Lossy Compression'
+        assert got == expected, "JPEG-lossy file, Code Meaning got %s, "\
+            "expected %s" % (got, expected)
+
+    def test_JPEGlossyPixelArray(self, jpeg_lossy):
+        a = jpeg_lossy.pixel_array
+        assert a.shape == (1024, 256)
+        # this test points were manually identified in Osirix viewer
+        assert a[420, 140] == 244
+        assert a[230, 120] == 95
+
+        assert a.flags.writeable
+
+    def test_JPEGBaselineColor3DPixelArray(self, color_3d_jpeg):
+        assert color_3d_jpeg.PhotometricInterpretation == "YBR_FULL_422"
+        a = color_3d_jpeg.pixel_array
+
+        assert a.flags.writeable
+
+        assert a.shape == (120, 480, 640, 3)
+        a = _convert_YBR_FULL_to_RGB(a)
         # this test points were manually identified in Osirix viewer
-        self.assertEqual(a[420, 140], 227)
-        self.assertEqual(a[230, 120], 105)
+        assert tuple(a[3, 159, 290, :]) == (41, 41, 41)
+        assert tuple(a[3, 169, 290, :]) == (57, 57, 57)
+        assert color_3d_jpeg.PhotometricInterpretation == "YBR_FULL_422"
+
+    @pytest.mark.parametrize(
+        "image,PhotometricInterpretation,results,convert_yuv_to_rgb",
+        pi_rgb_testdata,
+        ids=pi_rgb_test_ids)
+    def test_PI_RGB(self, image, PhotometricInterpretation, results,
+                    convert_yuv_to_rgb):
+        t = dcmread(image)
+        assert t.PhotometricInterpretation == PhotometricInterpretation
+        a = t.pixel_array
 
         assert a.flags.writeable
+
+        assert a.shape == (100, 100, 3)
+        if convert_yuv_to_rgb:
+            a = _convert_YBR_FULL_to_RGB(a)
+        # this test points are from the ImageComments tag
+        assert tuple(a[5, 50, :]) == results[0]
+        assert tuple(a[15, 50, :]) == results[1]
+        assert tuple(a[25, 50, :]) == results[2]
+        assert tuple(a[35, 50, :]) == results[3]
+        assert tuple(a[45, 50, :]) == results[4]
+        assert tuple(a[55, 50, :]) == results[5]
+        assert tuple(a[65, 50, :]) == results[6]
+        assert tuple(a[75, 50, :]) == results[7]
+        assert tuple(a[85, 50, :]) == results[8]
+        assert tuple(a[95, 50, :]) == results[9]
+        assert t.PhotometricInterpretation == PhotometricInterpretation
+
+
+class TestSupportFunctions():
+    @pytest.fixture(scope='class')
+    def dataset_2d(self):
+        return dcmread(mr_name)
+
+    @pytest.fixture(scope='class')
+    def dataset_2d_compressed(self):
+        return dcmread(jpeg2000_name)
+
+    @pytest.fixture(scope='class')
+    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 data_element.GetTag().GetGroup() == 0x7fe0
+        assert data_element.GetTag().GetElement() == 0x0010
+        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):
+        data_element = gdcm_handler.create_data_element(dataset_2d_compressed)
+
+        assert data_element.GetTag().GetGroup() == 0x7fe0
+        assert data_element.GetTag().GetElement() == 0x0010
+        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)
+
+        assert data_element.GetTag().GetGroup() == 0x7fe0
+        assert data_element.GetTag().GetElement() == 0x0010
+        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 image.GetNumberOfDimensions() == 2
+        assert image.GetDimensions() == [dataset_2d.Rows, dataset_2d.Columns]
+        assert image.GetPhotometricInterpretation().GetType() == \
+            gdcm.PhotometricInterpretation.GetPIType(
+                dataset_2d.PhotometricInterpretation)
+        assert image.GetTransferSyntax().GetString() == str.__str__(
+            dataset_2d.file_meta.TransferSyntaxUID)
+        pixel_format = image.GetPixelFormat()
+        assert pixel_format.GetSamplesPerPixel() == dataset_2d.SamplesPerPixel
+        assert pixel_format.GetBitsAllocated() == dataset_2d.BitsAllocated
+        assert pixel_format.GetBitsStored() == dataset_2d.BitsStored
+        assert pixel_format.GetHighBit() == dataset_2d.HighBit
+        assert pixel_format.GetPixelRepresentation() ==\
+            dataset_2d.PixelRepresentation
+
+    @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)
+        assert image.GetNumberOfDimensions() == 3
+        assert image.GetDimensions() == [
+            dataset_3d.Columns, dataset_3d.Rows,
+            int(dataset_3d.NumberOfFrames)]
+        assert image.GetPhotometricInterpretation().GetType() == \
+            gdcm.PhotometricInterpretation.GetPIType(
+                dataset_3d.PhotometricInterpretation)
+        assert image.GetTransferSyntax().GetString() == str.__str__(
+            dataset_3d.file_meta.TransferSyntaxUID)
+        pixel_format = image.GetPixelFormat()
+        assert pixel_format.GetSamplesPerPixel() == dataset_3d.SamplesPerPixel
+        assert pixel_format.GetBitsAllocated() == dataset_3d.BitsAllocated
+        assert pixel_format.GetBitsStored() == dataset_3d.BitsStored
+        assert pixel_format.GetHighBit() == dataset_3d.HighBit
+        assert pixel_format.GetPixelRepresentation() ==\
+            dataset_3d.PixelRepresentation
+        assert image.GetPlanarConfiguration() ==\
+            dataset_3d.PlanarConfiguration
+
+    @pytest.mark.skipif(not HAVE_GDCM, reason=gdcm_missing_message)
+    def test_create_image_reader_with_string(self):
+        image_reader = gdcm_handler.create_image_reader(mr_name)
+        assert image_reader is not None
+        assert image_reader.Read()
+
+    @pytest.mark.skipif(not HAVE_GDCM, reason=gdcm_missing_message)
+    @pytest.mark.skipif(not compat.in_py2, reason='Python2 specific')
+    def test_create_image_reader_with_py2_unicode_string(self):
+        filename = mr_name.decode('utf-8')
+        image_reader = gdcm_handler.create_image_reader(filename)
+        assert image_reader is not None
+        assert image_reader.Read()
diff --git a/pydicom/tests/test_handler_util.py b/pydicom/tests/test_handler_util.py
index f63ffe210..14e6e6512 100644
--- a/pydicom/tests/test_handler_util.py
+++ b/pydicom/tests/test_handler_util.py
@@ -18,7 +18,8 @@ from pydicom.pixel_data_handlers.util import (
     dtype_corrected_for_endianness,
     reshape_pixel_array,
     convert_color_space,
-    pixel_dtype
+    pixel_dtype,
+    get_expected_length
 )
 from pydicom.uid import (ExplicitVRLittleEndian,
                          UncompressedPixelTransferSyntaxes)
@@ -653,3 +654,105 @@ class TestNumpy_DtypeCorrectedForEndianness(object):
         with pytest.raises(ValueError,
                            match="attribute 'is_little_endian' has"):
             dtype_corrected_for_endianness(None, None)
+
+
+REFERENCE_LENGTH = [
+    # (frames, rows, cols, samples), bit depth, result in (bytes, pixels)
+    # No 'NumberOfFrames' in dataset
+    ((0, 0, 0, 0), 1, (0, 0)),
+    ((0, 1, 1, 1), 1, (1, 1)),  # 1 bit -> 1 byte
+    ((0, 1, 1, 3), 1, (1, 3)),  # 3 bits -> 1 byte
+    ((0, 1, 3, 3), 1, (2, 9)),  # 9 bits -> 2 bytes
+    ((0, 2, 2, 1), 1, (1, 4)),  # 4 bits -> 1 byte
+    ((0, 2, 4, 1), 1, (1, 8)),  # 8 bits -> 1 byte
+    ((0, 3, 3, 1), 1, (2, 9)),  # 9 bits -> 2 bytes
+    ((0, 512, 512, 1), 1, (32768, 262144)),  # Typical length
+    ((0, 512, 512, 3), 1, (98304, 786432)),
+    ((0, 0, 0, 0), 8, (0, 0)),
+    ((0, 1, 1, 1), 8, (1, 1)),  # Odd length
+    ((0, 9, 1, 1), 8, (9, 9)),  # Odd length
+    ((0, 1, 2, 1), 8, (2, 2)),  # Even length
+    ((0, 512, 512, 1), 8, (262144, 262144)),
+    ((0, 512, 512, 3), 8, (786432, 786432)),
+    ((0, 0, 0, 0), 16, (0, 0)),
+    ((0, 1, 1, 1), 16, (2, 1)),  # 16 bit data can't be odd length
+    ((0, 1, 2, 1), 16, (4, 2)),
+    ((0, 512, 512, 1), 16, (524288, 262144)),
+    ((0, 512, 512, 3), 16, (1572864, 786432)),
+    ((0, 0, 0, 0), 32, (0, 0)),
+    ((0, 1, 1, 1), 32, (4, 1)),  # 32 bit data can't be odd length
+    ((0, 1, 2, 1), 32, (8, 2)),
+    ((0, 512, 512, 1), 32, (1048576, 262144)),
+    ((0, 512, 512, 3), 32, (3145728, 786432)),
+    # NumberOfFrames odd
+    ((3, 0, 0, 0), 1, (0, 0)),
+    ((3, 1, 1, 1), 1, (1, 3)),
+    ((3, 1, 1, 3), 1, (2, 9)),
+    ((3, 1, 3, 3), 1, (4, 27)),
+    ((3, 2, 4, 1), 1, (3, 24)),
+    ((3, 2, 2, 1), 1, (2, 12)),
+    ((3, 3, 3, 1), 1, (4, 27)),
+    ((3, 512, 512, 1), 1, (98304, 786432)),
+    ((3, 512, 512, 3), 1, (294912, 2359296)),
+    ((3, 0, 0, 0), 8, (0, 0)),
+    ((3, 1, 1, 1), 8, (3, 3)),
+    ((3, 9, 1, 1), 8, (27, 27)),
+    ((3, 1, 2, 1), 8, (6, 6)),
+    ((3, 512, 512, 1), 8, (786432, 786432)),
+    ((3, 512, 512, 3), 8, (2359296, 2359296)),
+    ((3, 0, 0, 0), 16, (0, 0)),
+    ((3, 512, 512, 1), 16, (1572864, 786432)),
+    ((3, 512, 512, 3), 16, (4718592, 2359296)),
+    ((3, 0, 0, 0), 32, (0, 0)),
+    ((3, 512, 512, 1), 32, (3145728, 786432)),
+    ((3, 512, 512, 3), 32, (9437184, 2359296)),
+    # NumberOfFrames even
+    ((4, 0, 0, 0), 1, (0, 0)),
+    ((4, 1, 1, 1), 1, (1, 4)),
+    ((4, 1, 1, 3), 1, (2, 12)),
+    ((4, 1, 3, 3), 1, (5, 36)),
+    ((4, 2, 4, 1), 1, (4, 32)),
+    ((4, 2, 2, 1), 1, (2, 16)),
+    ((4, 3, 3, 1), 1, (5, 36)),
+    ((4, 512, 512, 1), 1, (131072, 1048576)),
+    ((4, 512, 512, 3), 1, (393216, 3145728)),
+    ((4, 0, 0, 0), 8, (0, 0)),
+    ((4, 512, 512, 1), 8, (1048576, 1048576)),
+    ((4, 512, 512, 3), 8, (3145728, 3145728)),
+    ((4, 0, 0, 0), 16, (0, 0)),
+    ((4, 512, 512, 1), 16, (2097152, 1048576)),
+    ((4, 512, 512, 3), 16, (6291456, 3145728)),
+    ((4, 0, 0, 0), 32, (0, 0)),
+    ((4, 512, 512, 1), 32, (4194304, 1048576)),
+    ((4, 512, 512, 3), 32, (12582912, 3145728)),
+]
+
+
+@pytest.mark.skipif(not HAVE_NP, reason="Numpy is not available")
+class TestNumpy_GetExpectedLength(object):
+    """Tests for numpy_handler.get_expected_length."""
+    @pytest.mark.parametrize('shape, bits, length', REFERENCE_LENGTH)
+    def test_length_in_bytes(self, shape, bits, length):
+        """Test get_expected_length(ds, unit='bytes')."""
+        ds = Dataset()
+        ds.Rows = shape[1]
+        ds.Columns = shape[2]
+        ds.BitsAllocated = bits
+        if shape[0] != 0:
+            ds.NumberOfFrames = shape[0]
+        ds.SamplesPerPixel = shape[3]
+
+        assert length[0] == get_expected_length(ds, unit='bytes')
+
+    @pytest.mark.parametrize('shape, bits, length', REFERENCE_LENGTH)
+    def test_length_in_pixels(self, shape, bits, length):
+        """Test get_expected_length(ds, unit='pixels')."""
+        ds = Dataset()
+        ds.Rows = shape[1]
+        ds.Columns = shape[2]
+        ds.BitsAllocated = bits
+        if shape[0] != 0:
+            ds.NumberOfFrames = shape[0]
+        ds.SamplesPerPixel = shape[3]
+
+        assert length[1] == get_expected_length(ds, unit='pixels')
diff --git a/pydicom/tests/test_numpy_pixel_data.py b/pydicom/tests/test_numpy_pixel_data.py
index cc38bbdd1..616a1af8b 100644
--- a/pydicom/tests/test_numpy_pixel_data.py
+++ b/pydicom/tests/test_numpy_pixel_data.py
@@ -56,7 +56,6 @@ try:
         get_pixeldata,
         unpack_bits,
         pack_bits,
-        get_expected_length,
         pixel_dtype,
     )
 except ImportError:
@@ -1098,105 +1097,3 @@ class TestNumpy_PackBits(object):
         arr = ds.pixel_array
         arr = arr.ravel()
         assert ds.PixelData == pack_bits(arr)
-
-
-REFERENCE_LENGTH = [
-    # (frames, rows, cols, samples), bit depth, result in (bytes, pixels)
-    # No 'NumberOfFrames' in dataset
-    ((0, 0, 0, 0), 1, (0, 0)),
-    ((0, 1, 1, 1), 1, (1, 1)),  # 1 bit -> 1 byte
-    ((0, 1, 1, 3), 1, (1, 3)),  # 3 bits -> 1 byte
-    ((0, 1, 3, 3), 1, (2, 9)),  # 9 bits -> 2 bytes
-    ((0, 2, 2, 1), 1, (1, 4)),  # 4 bits -> 1 byte
-    ((0, 2, 4, 1), 1, (1, 8)),  # 8 bits -> 1 byte
-    ((0, 3, 3, 1), 1, (2, 9)),  # 9 bits -> 2 bytes
-    ((0, 512, 512, 1), 1, (32768, 262144)),  # Typical length
-    ((0, 512, 512, 3), 1, (98304, 786432)),
-    ((0, 0, 0, 0), 8, (0, 0)),
-    ((0, 1, 1, 1), 8, (1, 1)),  # Odd length
-    ((0, 9, 1, 1), 8, (9, 9)),  # Odd length
-    ((0, 1, 2, 1), 8, (2, 2)),  # Even length
-    ((0, 512, 512, 1), 8, (262144, 262144)),
-    ((0, 512, 512, 3), 8, (786432, 786432)),
-    ((0, 0, 0, 0), 16, (0, 0)),
-    ((0, 1, 1, 1), 16, (2, 1)),  # 16 bit data can't be odd length
-    ((0, 1, 2, 1), 16, (4, 2)),
-    ((0, 512, 512, 1), 16, (524288, 262144)),
-    ((0, 512, 512, 3), 16, (1572864, 786432)),
-    ((0, 0, 0, 0), 32, (0, 0)),
-    ((0, 1, 1, 1), 32, (4, 1)),  # 32 bit data can't be odd length
-    ((0, 1, 2, 1), 32, (8, 2)),
-    ((0, 512, 512, 1), 32, (1048576, 262144)),
-    ((0, 512, 512, 3), 32, (3145728, 786432)),
-    # NumberOfFrames odd
-    ((3, 0, 0, 0), 1, (0, 0)),
-    ((3, 1, 1, 1), 1, (1, 3)),
-    ((3, 1, 1, 3), 1, (2, 9)),
-    ((3, 1, 3, 3), 1, (4, 27)),
-    ((3, 2, 4, 1), 1, (3, 24)),
-    ((3, 2, 2, 1), 1, (2, 12)),
-    ((3, 3, 3, 1), 1, (4, 27)),
-    ((3, 512, 512, 1), 1, (98304, 786432)),
-    ((3, 512, 512, 3), 1, (294912, 2359296)),
-    ((3, 0, 0, 0), 8, (0, 0)),
-    ((3, 1, 1, 1), 8, (3, 3)),
-    ((3, 9, 1, 1), 8, (27, 27)),
-    ((3, 1, 2, 1), 8, (6, 6)),
-    ((3, 512, 512, 1), 8, (786432, 786432)),
-    ((3, 512, 512, 3), 8, (2359296, 2359296)),
-    ((3, 0, 0, 0), 16, (0, 0)),
-    ((3, 512, 512, 1), 16, (1572864, 786432)),
-    ((3, 512, 512, 3), 16, (4718592, 2359296)),
-    ((3, 0, 0, 0), 32, (0, 0)),
-    ((3, 512, 512, 1), 32, (3145728, 786432)),
-    ((3, 512, 512, 3), 32, (9437184, 2359296)),
-    # NumberOfFrames even
-    ((4, 0, 0, 0), 1, (0, 0)),
-    ((4, 1, 1, 1), 1, (1, 4)),
-    ((4, 1, 1, 3), 1, (2, 12)),
-    ((4, 1, 3, 3), 1, (5, 36)),
-    ((4, 2, 4, 1), 1, (4, 32)),
-    ((4, 2, 2, 1), 1, (2, 16)),
-    ((4, 3, 3, 1), 1, (5, 36)),
-    ((4, 512, 512, 1), 1, (131072, 1048576)),
-    ((4, 512, 512, 3), 1, (393216, 3145728)),
-    ((4, 0, 0, 0), 8, (0, 0)),
-    ((4, 512, 512, 1), 8, (1048576, 1048576)),
-    ((4, 512, 512, 3), 8, (3145728, 3145728)),
-    ((4, 0, 0, 0), 16, (0, 0)),
-    ((4, 512, 512, 1), 16, (2097152, 1048576)),
-    ((4, 512, 512, 3), 16, (6291456, 3145728)),
-    ((4, 0, 0, 0), 32, (0, 0)),
-    ((4, 512, 512, 1), 32, (4194304, 1048576)),
-    ((4, 512, 512, 3), 32, (12582912, 3145728)),
-]
-
-
-@pytest.mark.skipif(not HAVE_NP, reason="Numpy is not available")
-class TestNumpy_GetExpectedLength(object):
-    """Tests for numpy_handler.get_expected_length."""
-    @pytest.mark.parametrize('shape, bits, length', REFERENCE_LENGTH)
-    def test_length_in_bytes(self, shape, bits, length):
-        """Test get_expected_length(ds, unit='bytes')."""
-        ds = Dataset()
-        ds.Rows = shape[1]
-        ds.Columns = shape[2]
-        ds.BitsAllocated = bits
-        if shape[0] != 0:
-            ds.NumberOfFrames = shape[0]
-        ds.SamplesPerPixel = shape[3]
-
-        assert length[0] == get_expected_length(ds, unit='bytes')
-
-    @pytest.mark.parametrize('shape, bits, length', REFERENCE_LENGTH)
-    def test_length_in_pixels(self, shape, bits, length):
-        """Test get_expected_length(ds, unit='pixels')."""
-        ds = Dataset()
-        ds.Rows = shape[1]
-        ds.Columns = shape[2]
-        ds.BitsAllocated = bits
-        if shape[0] != 0:
-            ds.NumberOfFrames = shape[0]
-        ds.SamplesPerPixel = shape[3]
-
-        assert length[1] == get_expected_length(ds, unit='pixels')