Repository: imageio/imageio

Issue:
Screengrab plugin
Pillow can apparently do screen-grabs. That would be a nice addition to the webcam support.

Discussion:

Patch:
diff --git a/docs/examples.rst b/docs/examples.rst
index 4a25d4f..f2a1f24 100644
--- a/docs/examples.rst
+++ b/docs/examples.rst
@@ -49,6 +49,19 @@ Iterate over frames in a movie
         print('Mean of frame %i is %1.1f' % (i, im.mean()))
 
 
+Grab screenshot or image from the clipboard
+-------------------------------------------
+
+(Screenshots are supported on Windows and OS X, clipboard on Windows only.)
+
+.. code-block:: python
+    
+    import imageio
+    
+    im_screen = imageio.imread('<screen>')
+    im_clipboard = imageio.imread('<clipboard>')
+
+
 Grab frames from your webcam
 ----------------------------
 
diff --git a/imageio/core/functions.py b/imageio/core/functions.py
index d07409b..653d6fe 100644
--- a/imageio/core/functions.py
+++ b/imageio/core/functions.py
@@ -33,12 +33,31 @@ be used to read/write data and meta data in a more controlled manner.
 This also allows specific scientific formats to be exposed in a way
 that best suits that file-format.
 
+----
 
-.. note::
-    
-    Some of these functions were renamed in v1.1 to realize a more clear
-    and consistent API. The old functions are still available for
-    backward compatibility (and will be in the foreseeable future).
+Supported resource URI's:
+
+All functions described here accept a URI to describe the resource to
+read from or write to. These can be a wide range of things. (Imageio
+takes care of handling the URI so that plugins can access the data in
+an easy way.)
+
+For reading and writing:
+
+* a normal filename, e.g. ``'c:\\foo\\bar.png'``
+* a file in a zipfile, e.g. ``'c:\\foo\\bar.zip\\eggs.png'``
+* a file object with a ``read()`` / ``write()`` method.
+
+For reading:
+
+* an http/ftp address, e.g. ``'http://example.com/foo.png'``
+* the raw bytes of an image file
+* ``get_reader("<video0>")`` to grab images from a (web) camera.
+* ``imread("<screen>")`` to grab a screenshot (on Windows or OS X).
+* ``imread("<clipboard>")`` to grab an image from the clipboard (on Windows).
+
+For writing one can also use ``'<bytes>'`` or ``imageio.RETURN_BYTES`` to
+make a write function return the bytes instead of writing to a file.
 
 """
 
@@ -80,9 +99,8 @@ def get_reader(uri, format=None, mode='?', **kwargs):
     Parameters
     ----------
     uri : {str, bytes, file}
-        The resource to load the image from. This can be a normal
-        filename, a file in a zipfile, an http/ftp address, a file
-        object, or the raw bytes of an image file.
+        The resource to load the image from, e.g. a filename, http address or
+        file object, see the docs for more info.
     format : str
         The format to use to read the file. By default imageio selects
         the appropriate for you based on the filename and its contents.
@@ -120,10 +138,8 @@ def get_writer(uri, format=None, mode='?', **kwargs):
     Parameters
     ----------
     uri : {str, file}
-        The resource to write the image to. This can be a normal
-        filename, a file in a zipfile, a file object, or
-        ``imageio.RETURN_BYTES``, in which case the raw bytes are
-        returned.
+        The resource to write the image to, e.g. a filename or file object,
+        see the docs for more info.
     format : str
         The format to use to read the file. By default imageio selects
         the appropriate for you based on the filename.
@@ -170,9 +186,8 @@ def imread(uri, format=None, **kwargs):
     Parameters
     ----------
     uri : {str, bytes, file}
-        The resource to load the image from. This can be a normal
-        filename, a file in a zipfile, an http/ftp address, a file
-        object, or the raw bytes.
+        The resource to load the image from, e.g. a filename, http address or
+        file object, see the docs for more info.
     format : str
         The format to use to read the file. By default imageio selects
         the appropriate for you based on the filename and its contents.
@@ -195,10 +210,8 @@ def imwrite(uri, im, format=None, **kwargs):
     Parameters
     ----------
     uri : {str, file}
-        The resource to write the image to. This can be a normal
-        filename, a file in a zipfile, a file object, or
-        ``imageio.RETURN_BYTES``, in which case the raw bytes are
-        returned.
+        The resource to write the image to, e.g. a filename or file object,
+        see the docs for more info.
     im : numpy.ndarray
         The image data. Must be NxM, NxMx3 or NxMx4.
     format : str
@@ -240,9 +253,8 @@ def mimread(uri, format=None, memtest=True, **kwargs):
     Parameters
     ----------
     uri : {str, bytes, file}
-        The resource to load the images from. This can be a normal
-        filename, a file in a zipfile, an http/ftp address, a file
-        object, or the raw bytes.
+        The resource to load the images from, e.g. a filename, http address or
+        file object, see the docs for more info.
     format : str
         The format to use to read the file. By default imageio selects
         the appropriate for you based on the filename and its contents.
@@ -284,10 +296,8 @@ def mimwrite(uri, ims, format=None, **kwargs):
     Parameters
     ----------
     uri : {str, file}
-        The resource to write the images to. This can be a normal
-        filename, a file in a zipfile, a file object, or
-        ``imageio.RETURN_BYTES``, in which case the raw bytes are
-        returned.
+        The resource to write the images to, e.g. a filename or file object,
+        see the docs for more info.
     ims : sequence of numpy arrays
         The image data. Each array must be NxM, NxMx3 or NxMx4.
     format : str
@@ -339,9 +349,8 @@ def volread(uri, format=None, **kwargs):
     Parameters
     ----------
     uri : {str, bytes, file}
-        The resource to load the volume from. This can be a normal
-        filename, a file in a zipfile, an http/ftp address, a file
-        object, or the raw bytes.
+        The resource to load the volume from, e.g. a filename, http address or
+        file object, see the docs for more info.
     format : str
         The format to use to read the file. By default imageio selects
         the appropriate for you based on the filename and its contents.
@@ -364,10 +373,8 @@ def volwrite(uri, im, format=None, **kwargs):
     Parameters
     ----------
     uri : {str, file}
-        The resource to write the image to. This can be a normal
-        filename, a file in a zipfile, a file object, or
-        ``imageio.RETURN_BYTES``, in which case the raw bytes are
-        returned.
+        The resource to write the image to, e.g. a filename or file object,
+        see the docs for more info.
     vol : numpy.ndarray
         The image data. Must be NxMxL (or NxMxLxK if each voxel is a tuple).
     format : str
@@ -410,9 +417,8 @@ def mvolread(uri, format=None, memtest=True, **kwargs):
     Parameters
     ----------
     uri : {str, bytes, file}
-        The resource to load the volumes from. This can be a normal
-        filename, a file in a zipfile, an http/ftp address, a file
-        object, or the raw bytes.
+        The resource to load the volumes from, e.g. a filename, http address or
+        file object, see the docs for more info.
     format : str
         The format to use to read the file. By default imageio selects
         the appropriate for you based on the filename and its contents.
@@ -453,10 +459,8 @@ def mvolwrite(uri, ims, format=None, **kwargs):
     Parameters
     ----------
     uri : {str, file}
-        The resource to write the volumes to. This can be a normal
-        filename, a file in a zipfile, a file object, or
-        ``imageio.RETURN_BYTES``, in which case the raw bytes are
-        returned.
+        The resource to write the volumes to, e.g. a filename or file object,
+        see the docs for more info.
     ims : sequence of numpy arrays
         The image data. Each array must be NxMxL (or NxMxLxK if each
         voxel is a tuple).
diff --git a/imageio/core/request.py b/imageio/core/request.py
index bc07b25..c77b067 100644
--- a/imageio/core/request.py
+++ b/imageio/core/request.py
@@ -26,6 +26,8 @@ URI_ZIPPED = 4
 URI_HTTP = 5
 URI_FTP = 6
 
+SPECIAL_READ_URIS = '<video', '<screen>', '<clipboard>'
+
 # The user can use this string in a write call to get the data back as bytes.
 RETURN_BYTES = '<bytes>'
 
@@ -148,7 +150,7 @@ class Request(object):
             elif uri.startswith('file://'):
                 self._uri_type = URI_FILENAME
                 self._filename = uri[7:]
-            elif uri.startswith('<video') and is_read_request:
+            elif uri.startswith(SPECIAL_READ_URIS) and is_read_request:
                 self._uri_type = URI_BYTES
                 self._filename = uri
             elif uri.startswith(RETURN_BYTES) and is_write_request:
diff --git a/imageio/plugins/__init__.py b/imageio/plugins/__init__.py
index 160647b..ee86ffc 100644
--- a/imageio/plugins/__init__.py
+++ b/imageio/plugins/__init__.py
@@ -83,6 +83,7 @@ For the Format.Writer class:
 # First import plugins that we want to take precedence over freeimage
 from . import tifffile
 from . import pillow
+from . import grab
 
 from . import freeimage
 from . import freeimagemulti
diff --git a/imageio/plugins/grab.py b/imageio/plugins/grab.py
new file mode 100644
index 0000000..9d08d11
--- /dev/null
+++ b/imageio/plugins/grab.py
@@ -0,0 +1,123 @@
+"""
+PIL-based formats to take screenshots and grab from the clipboard.
+"""
+
+from __future__ import absolute_import, print_function, division
+
+import threading
+
+import numpy as np
+
+from .. import formats
+from ..core import Format
+
+
+class BaseGrabFormat(Format):
+    """ Base format for grab formats.
+    """
+    
+    _pillow_imported = False
+    _ImageGrab = None
+    
+    def __init__(self, *args, **kwargs):
+        super(BaseGrabFormat, self).__init__(*args, **kwargs)
+        self._lock = threading.RLock()
+    
+    def _can_write(self, request):
+        return False
+    
+    def _init_pillow(self):
+        with self._lock:
+            if not self._pillow_imported:
+                self._pillow_imported = True  # more like tried to import
+                import PIL
+                if not hasattr(PIL, 'PILLOW_VERSION'):  # pragma: no cover
+                    raise ImportError('Imageio Pillow requires '
+                                      'Pillow, not PIL!')
+                try:
+                    from PIL import ImageGrab
+                except ImportError:
+                    return None
+                self._ImageGrab = ImageGrab
+        return self._ImageGrab
+    
+    class Reader(Format.Reader):
+        
+        def _open(self):
+            pass
+        
+        def _close(self):
+            pass
+        
+        def _get_data(self, index):
+            return self.format._get_data(index)
+
+
+class ScreenGrabFormat(BaseGrabFormat):
+    """ The ScreenGrabFormat provided a means to grab screenshots using
+    the uri of "<screen>".
+    
+    This functionality is provided via Pillow. Note that "<screen>" is
+    only supported on Windows and OS X.
+    
+    Parameters for reading
+    ----------------------
+    No parameters.
+    """
+    
+    def _can_read(self, request):
+        if request.mode[1] not in 'i?':
+            return False
+        if request.filename != '<screen>':
+            return False
+        return bool(self._init_pillow())
+    
+    def _get_data(self, index):
+        ImageGrab = self._init_pillow()
+        assert ImageGrab
+         
+        pil_im = ImageGrab.grab()
+        assert pil_im is not None
+        im = np.asarray(pil_im)
+        return im, {}
+
+
+class ClipboardGrabFormat(BaseGrabFormat):
+    """ The ClipboardGrabFormat provided a means to grab image data from
+    the clipboard, using the uri "<clipboard>"
+    
+    This functionality is provided via Pillow. Note that "<clipboard>" is
+    only supported on Windows.
+    
+    Parameters for reading
+    ----------------------
+    No parameters.
+    """
+    
+    def _can_read(self, request):
+        if request.mode[1] not in 'i?':
+            return False
+        if request.filename != '<clipboard>':
+            return False
+        return bool(self._init_pillow())
+    
+    def _get_data(self, index):
+        ImageGrab = self._init_pillow()
+        assert ImageGrab
+        
+        pil_im = ImageGrab.grabclipboard()
+        if pil_im is None:
+            raise RuntimeError('There seems to be no image data on the '
+                               'clipboard now.')
+        im = np.asarray(pil_im)
+        return im, {}
+
+
+# Register. You register an *instance* of a Format class.
+format = ScreenGrabFormat('screengrab',
+                          'Grab screenshots (Windows and OS X only)', [], 'i')
+formats.add_format(format)
+
+format = ClipboardGrabFormat('clipboardgrab',
+                             'Grab from clipboard (Windows only)', [], 'i')
+formats.add_format(format)
diff --git a/imageio/plugins/pillow.py b/imageio/plugins/pillow.py
index ac9ce19..561a76c 100644
--- a/imageio/plugins/pillow.py
+++ b/imageio/plugins/pillow.py
@@ -46,12 +46,12 @@ class PillowFormat(Format):
             if not self._pillow_imported:
                 self._pillow_imported = True  # more like tried to import
                 import PIL
-                if not hasattr(PIL, 'PILLOW_VERSION'):
+                if not hasattr(PIL, 'PILLOW_VERSION'):  # pragma: no cover
                     raise ImportError('Imageio Pillow requires '
                                       'Pillow, not PIL!')
                 from PIL import Image
                 self._Image = Image
-            elif self._Image is None:
+            elif self._Image is None:  # pragma: no cover
                 raise RuntimeError('Imageio Pillow plugin requires '
                                    'Pillow lib.')
             Image = self._Image
diff --git a/imageio/plugins/pillow_info.py b/imageio/plugins/pillow_info.py
index 89a9a24..1b5a1b0 100644
--- a/imageio/plugins/pillow_info.py
+++ b/imageio/plugins/pillow_info.py
@@ -10,7 +10,7 @@ if run as a script.
 """
 
 
-def generate_info():
+def generate_info():  # pragma: no cover
     from urllib.request import urlopen
     import PIL
     from PIL import Image
@@ -124,8 +124,8 @@ pillow_formats = [
     ('IM', 'IFUNC Image Memory', '.im'),
     ('IMT', 'IM Tools', ''),
     ('IPTC', 'IPTC/NAA', '.iim'),
-    ('JPEG', 'JPEG (ISO 10918)', '.jfif .jpeg .jpg .jpe'),
-    ('JPEG2000', 'JPEG 2000 (ISO 15444)', '.jpf .j2c .jpc .jp2 .j2k .jpx'),
+    ('JPEG', 'JPEG (ISO 10918)', '.jfif .jpe .jpg .jpeg'),
+    ('JPEG2000', 'JPEG 2000 (ISO 15444)', '.jp2 .j2k .jpc .jpf .jpx .j2c'),
     ('MCIDAS', 'McIdas area file', ''),
     ('MIC', 'Microsoft Image Composer', '.mic'),
     ('MPEG', 'MPEG', '.mpg .mpeg'),
@@ -135,14 +135,13 @@ pillow_formats = [
     ('PCX', 'Paintbrush', '.pcx'),
     ('PIXAR', 'PIXAR raster image', '.pxr'),
     ('PNG', 'Portable network graphics', '.png'),
-    ('PPM', 'Pbmplus image', '.ppm .pgm .pbm'),
+    ('PPM', 'Pbmplus image', '.pbm .pgm .ppm'),
     ('PSD', 'Adobe Photoshop', '.psd'),
-    ('SGI', 'SGI Image File Format', '.rgb .sgi .bw .rgba'),
+    ('SGI', 'SGI Image File Format', '.bw .rgb .rgba .sgi'),
     ('SPIDER', 'Spider 2D image', ''),
     ('SUN', 'Sun Raster File', '.ras'),
     ('TGA', 'Targa', '.tga'),
     ('TIFF', 'Adobe TIFF', '.tif .tiff'),
-    ('WEBP', 'WebP image', '.webp'),
     ('WMF', 'Windows Metafile', '.wmf .emf'),
     ('XBM', 'X11 Bitmap', '.xbm'),
     ('XPM', 'X11 Pixel Map', '.xpm'),
@@ -317,9 +316,6 @@ u"""*This is a copy from the Pillow docs.*
     **background**
         Default background color (a palette color index).
     
-    **duration**
-        Time between frames in an animation (in milliseconds).
-    
     **transparency**
         Transparency color index. This key is omitted if the image is not
         transparent.
@@ -328,8 +324,8 @@ u"""*This is a copy from the Pillow docs.*
         Version (either ``GIF87a`` or ``GIF89a``).
     
     **duration**
-        May not be present. The time to display each frame of the GIF, in
-        milliseconds.
+        May not be present. The time to display the current frame
+        of the GIF, in milliseconds.
     
     **loop**
         May not be present. The number of times the GIF should loop.
@@ -343,20 +339,41 @@ u"""*This is a copy from the Pillow docs.*
     
     ``im.seek()`` raises an ``EOFError`` if you try to seek after the last frame.
     
-    Saving sequences
-    ~~~~~~~~~~~~~~~~
+    Saving
+    ~~~~~~
     
-    When calling :py:meth:`~PIL.Image.Image.save`, if a multiframe image is used,
-    by default only the first frame will be saved. To save all frames, the
-    ``save_all`` parameter must be present and set to ``True``. To append
-    additional frames when saving, the ``append_images`` parameter works with
-    ``save_all`` to append a list of images containing the extra frames::
+    When calling :py:meth:`~PIL.Image.Image.save`, the following options
+    are available::
     
         im.save(out, save_all=True, append_images=[im1, im2, ...])
     
-    If present, the ``loop`` parameter can be used to set the number of times
-    the GIF should loop, and the ``duration`` parameter can set the number of
-    milliseconds between each frame.
+    **save_all**
+        If present and true, all frames of the image will be saved. If
+        not, then only the first frame of a multiframe image will be saved.
+    
+    **append_images**
+        A list of images to append as additional frames. Each of the
+        images in the list can be single or multiframe images.
+    
+    **duration**
+        The display duration of each frame of the multiframe gif, in
+        milliseconds. Pass a single integer for a constant duration, or a
+        list or tuple to set the duration for each frame separately.
+    
+    **loop**
+        Integer number of times the GIF should loop.
+    
+    **optimize**
+        If present and true, attempt to compress the palette by
+        eliminating unused colors. This is only useful if the palette can
+        be compressed to the next smaller power of 2 elements.
+    
+    **palette** 
+        Use the specified palette for the saved image. The palette should
+        be a bytes or bytearray object containing the palette entries in
+        RGBRGB... form. It should be no more than 768 bytes. Alternately,
+        the palette can be passed in as an
+        :py:class:`PIL.ImagePalette.ImagePalette` object.
     
     Reading local images
     ~~~~~~~~~~~~~~~~~~~~
@@ -797,12 +814,14 @@ u"""*This is a copy from the Pillow docs.*
 
     
     PIL identifies and reads PSD files written by Adobe Photoshop 2.5 and 3.0.
+    
     """,
 'SGI':
 u"""*This is a copy from the Pillow docs.*
 
     
-    PIL reads uncompressed ``L``, ``RGB``, and ``RGBA`` files.
+    Pillow reads and writes uncompressed ``L``, ``RGB``, and ``RGBA`` files.
+    
     """,
 'SPIDER':
 u"""*This is a copy from the Pillow docs.*
@@ -842,8 +861,8 @@ u"""*This is a copy from the Pillow docs.*
     For more information about the SPIDER image processing package, see the
     `SPIDER homepage`_ at `Wadsworth Center`_.
     
-    .. _SPIDER homepage: http://spider.wadsworth.org/spider_doc/spider/docs/spider.html
-    .. _Wadsworth Center: http://www.wadsworth.org/
+    .. _SPIDER homepage: https://spider.wadsworth.org/spider_doc/spider/docs/spider.html
+    .. _Wadsworth Center: https://www.wadsworth.org/
     """,
 'SUN':
 u"""No docs for SUN.""",
@@ -981,30 +1000,6 @@ u"""*This is a copy from the Pillow docs.*
         an equal x and y resolution, dpi also implies a unit of inches.
     
     """,
-'WEBP':
-u"""*This is a copy from the Pillow docs.*
-
-    
-    PIL reads and writes WebP files. The specifics of PIL's capabilities with this
-    format are currently undocumented.
-    
-    The :py:meth:`~PIL.Image.Image.save` method supports the following options:
-    
-    **lossless**
-        If present and true, instructs the WEBP writer to use lossless compression.
-    
-    **quality**
-        Integer, 1-100, Defaults to 80. Sets the quality level for
-        lossy compression.
-    
-    **icc_procfile**
-        The ICC Profile to include in the saved file. Only supported if
-        the system webp library was built with webpmux support.
-    
-    **exif**
-        The exif data to include in the saved file. Only supported if
-        the system webp library was built with webpmux support.
-    """,
 'WMF':
 u"""*This is a copy from the Pillow docs.*
 
diff --git a/imageio/plugins/tifffile.py b/imageio/plugins/tifffile.py
index a26ea79..f6d25ad 100644
--- a/imageio/plugins/tifffile.py
+++ b/imageio/plugins/tifffile.py
@@ -37,11 +37,8 @@ READ_METADATA_KEYS = ('planar_configuration', 'is_fluoview', 'is_nih',
 
 
 class TiffFormat(Format):
+
     """ Provides support for a wide range of Tiff images.
-    
-    Images that contain multiple pages can be read using ``imageio.mimread()``
-    to read the individual pages, or ``imageio.volread()`` to obtain a
-    single (higher dimensional) array.
 
     Parameters for reading
     ----------------------
@@ -180,28 +177,17 @@ class TiffFormat(Format):
 
         def _close(self):
             self._tf.close()
-        
+
         def _get_length(self):
-            if self.request.mode[1] in 'vV':
-                return 1  # or can there be pages in pages or something?
-            else:
-                return len(self._tf)
-        
+            return len(self._tf)
+
         def _get_data(self, index):
-            if self.request.mode[1] in 'vV':
-                # Read data as single 3D (+ color channels) array
-                if index != 0:
-                    raise IndexError(
-                        'Tiff support no more than 1 "volume" per file')
-                im = self._tf.asarray()  # request as singleton image
-                meta = self._meta
-            else:
-                # Read as 2D image
-                if index < 0 or index >= len(self._tf):
-                    raise IndexError(
-                        'Index out of range while reading from tiff file')
-                im = self._tf[index].asarray()
-                meta = self._meta or self._get_meta_data(index)
+            # Get data
+            if index < 0 or index >= len(self._tf):
+                raise IndexError(
+                    'Index out of range while reading from tiff file')
+            im = self._tf[index].asarray()
+            meta = self._meta or self._get_meta_data(index)
             # Return array and empty meta data
             return im, meta
 
@@ -230,8 +216,6 @@ class TiffFormat(Format):
         def _append_data(self, im, meta):
             if meta:
                 self.set_meta_data(meta)
-            # No need to check self.request.mode; tiffile figures out whether
-            # this is a single page, or all page data at once.
             self._tf.save(np.asanyarray(im), **self._meta)
 
         def set_meta_data(self, meta):


Test patch:
diff --git a/tests/test_grab.py b/tests/test_grab.py
new file mode 100644
index 0000000..1cb41bc
--- /dev/null
+++ b/tests/test_grab.py
@@ -0,0 +1,98 @@
+import sys
+
+import numpy as np
+
+from pytest import raises
+from imageio.testing import run_tests_if_main
+
+import imageio
+
+
+def test_grab_plugin_load():
+    
+    imageio.plugins.grab.BaseGrabFormat._ImageGrab = FakeImageGrab
+    imageio.plugins.grab.BaseGrabFormat._pillow_imported = True
+    _plat = sys.platform
+    sys.platform = 'win32'
+    
+    try:
+        
+        reader = imageio.get_reader('<screen>')
+        assert reader.format.name == 'SCREENGRAB'
+        
+        reader = imageio.get_reader('<clipboard>')
+        assert reader.format.name == 'CLIPBOARDGRAB'
+    
+        with raises(ValueError):
+            imageio.get_writer('<clipboard>')
+        with raises(ValueError):
+            imageio.get_writer('<screen>')
+    
+    finally:
+        sys.platform = _plat
+        imageio.plugins.grab.BaseGrabFormat._ImageGrab = None
+        imageio.plugins.grab.BaseGrabFormat._pillow_imported = False
+
+
+class FakeImageGrab:
+    
+    has_clipboard = True
+    
+    @classmethod
+    def grab(cls):
+        return np.zeros((8, 8, 3), np.uint8)
+    
+    @classmethod
+    def grabclipboard(cls):
+        if cls.has_clipboard:
+            return np.zeros((9, 9, 3), np.uint8)
+        else:
+            return None
+
+
+def test_grab_simulated():
+    # Hard to test for real, if only because its only fully suppored on
+    # Windows, but we can monkey patch so we can test all the imageio bits.
+    
+    imageio.plugins.grab.BaseGrabFormat._ImageGrab = FakeImageGrab
+    imageio.plugins.grab.BaseGrabFormat._pillow_imported = True
+    _plat = sys.platform
+    sys.platform = 'win32'
+    
+    try:
+        
+        im = imageio.imread('<screen>')
+        assert im.shape == (8, 8, 3)
+        
+        reader = imageio.get_reader('<screen>')
+        im1 = reader.get_data(0)
+        im2 = reader.get_data(0)
+        im3 = reader.get_data(1)
+        assert im1.shape == (8, 8, 3)
+        assert im2.shape == (8, 8, 3)
+        assert im3.shape == (8, 8, 3)
+        
+        im = imageio.imread('<clipboard>')
+        assert im.shape == (9, 9, 3)
+        
+        reader = imageio.get_reader('<clipboard>')
+        im1 = reader.get_data(0)
+        im2 = reader.get_data(0)
+        im3 = reader.get_data(1)
+        assert im1.shape == (9, 9, 3)
+        assert im2.shape == (9, 9, 3)
+        assert im3.shape == (9, 9, 3)
+        
+        # Grabbing from clipboard can fail if there is no image data to grab
+        FakeImageGrab.has_clipboard = False
+        with raises(RuntimeError):
+            im = imageio.imread('<clipboard>')
+    
+    finally:
+        sys.platform = _plat
+        imageio.plugins.grab.BaseGrabFormat._ImageGrab = None
+        imageio.plugins.grab.BaseGrabFormat._pillow_imported = False
+        FakeImageGrab.has_clipboard = True
+
+
+run_tests_if_main()
diff --git a/tests/test_tifffile.py b/tests/test_tifffile.py
index ea7ad7d..5ab5ce7 100644
--- a/tests/test_tifffile.py
+++ b/tests/test_tifffile.py
@@ -34,29 +34,16 @@ def test_tifffile_reading_writing():
     imageio.imsave(filename1, im2)
     im = imageio.imread(filename1)
     ims = imageio.mimread(filename1)
-    assert im.shape == im2.shape
     assert (im == im2).all()
     assert len(ims) == 1
-    
+
     # Multiple images
     imageio.mimsave(filename1, [im2, im2, im2])
     im = imageio.imread(filename1)
     ims = imageio.mimread(filename1)
-    assert im.shape == im2.shape
-    assert (im == im2).all()  # note: this does not imply that the shape match!
-    assert len(ims) == 3
-    for i in range(3):
-        assert ims[i].shape == im2.shape
-        assert (ims[i] == im2).all()
-
-    # Read all planes as one array - we call it a volume for clarity
-    vol = imageio.volread(filename1)
-    vols = imageio.mvolread(filename1)
-    assert vol.shape == (3, ) + im2.shape
-    assert len(vols) == 1 and vol.shape == vols[0].shape
-    for i in range(3):
-        assert (vol[i] == im2).all()
-    
+    assert (im == im2).all()
+    assert len(ims) == 3, ims[0].shape
+
     # remote multipage rgb file
     filename2 = get_remote_file('images/multipage_rgb.tif')
     img = imageio.mimread(filename2)
@@ -80,24 +67,13 @@ def test_tifffile_reading_writing():
     # Fail
     raises(IndexError, R.get_data, -1)
     raises(IndexError, R.get_data, 3)
-    
-    # Ensure imread + imwrite works round trip
-    filename3 = os.path.join(test_dir, 'test_tiff2.tiff')
-    im1 = imageio.imread(filename1)
-    imageio.imwrite(filename3, im1)
-    im3 = imageio.imread(filename3)
-    assert im1.ndim == 3
-    assert im1.shape == im3.shape
-    assert (im1 == im3).all()
-    
-    # Ensure imread + imwrite works round trip - volume like
+
+    # Ensure imwrite write works round trip
     filename3 = os.path.join(test_dir, 'test_tiff2.tiff')
-    im1 = imageio.volread(filename1)
-    imageio.volwrite(filename3, im1)
-    im3 = imageio.volread(filename3)
-    assert im1.ndim == 4
-    assert im1.shape == im3.shape
-    assert (im1 == im3).all()
+    R = imageio.imread(filename1)
+    imageio.imwrite(filename3, R)
+    R2 = imageio.imread(filename3)
+    assert (R == R2).all()
 
 
 run_tests_if_main()