Repository: python-pillow/Pillow

Issue:
ImageGrab on Linux
### What did you do?

Got Ubuntu 16.04.

```python
>>> from PIL import ImageGrab
```
### What did you expect to happen?

```python
>>>
```
### What actually happened?

```python
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/lib/python2.7/dist-packages/PIL/ImageGrab.py", line 22, in <module>
    raise ImportError("ImageGrab is OS X and Windows only")
```
### What versions of Pillow and Python are you using?

```python
>>> PIL.VERSION
'1.1.7'
>>> PIL.PILLOW_VERSION
'3.1.2'
```

So, what is the proper way for saving screens on Linux?



Discussion:
Found this - http://stackoverflow.com/a/16141058/239247 - not sure if that is a good way.

Second thought:
pyscreenshot https://pypi.python.org/pypi/pyscreenshot is a replacement module for ImageGrab.
But yes, it would be nice to have ImageGrab completely cross compatible in the Pillow library. 
Maybe try asking the author to license it under PIL/Pillow license.
Or look at the code and review what changes might need to ImageGrab and implement them similarly in the module.

https://github.com/BoboTiG/python-mss

it seems to do all the work itself without calling other tools. it claims to work on all platforms. cannot test because i do not have windows nor mac.

license is zlib/libpng. do not know how easy it is to integrate code in PIL/Pillow MIT like license.

pyscreenshot seems to be "just" a python wrapper around other tools. a quick glance in the source code revealed many subprocess calls (via EasyProcess). ironically PIL/Pillow itself is one of the wrapped tools (only on windows).

I am the developer of the [MSS](https://github.com/BoboTiG/python-mss) module. I changed the license for MIT in the `dev` branch. I added a good documentation and tests are coming. If you are interested in the use of my work and need few adjustments in the API, we can talk :)

@BoboTiG basically Pillow needs to know what is the format of raw data that is returned from some screenshot call to give it a type. Another issue is that PIL code doesn't have any install dependencies, so either include the code as-is, or make it as optional dependency. @aclark4life should know better what is the proper way to resolve this with PR.

The format is always simple "RGB".  For the unique dependence, it is optional but will boost the screenshot. I am still working on to speed-up the process.

Another update to notify that MSS has no more dependencies. Changes are in the `dev` branch. Also, I created an [example using PIL](http://python-mss.readthedocs.io/en/dev/examples.html#pil). 

[pyscreenshot](https://github.com/ponty/pyscreenshot) library can be used
Refer here : https://stackoverflow.com/a/43636492/5463601
I'm interested in getting this working, but curious which approach to take since I don't own either of the libraries:
- copy-paste code from mss into pillow (all the users get fast, cross-platform screenshots all the time, but code is duplicated between pillow and mss - I'm not sure if @BoboTiG is happy to have his code "stolen" like this, or if the Pillow maintainers are happy to maintain the code; but as an end user I like this approach because everything is awesome out of the box)
- add mss as an optional dependency from pillow, and have ImageGrab just become a simple wrapper, replacing the current code, raising an exception if mss isn't installed (users get fast cross-platform screenshots, but they need to install an extra library)
- add mss as an optional dependency, and keep the current ImageGrab code as a fallback (the most compatibility; but ImageGrab will be slow by default, and there's more code to maintain, and there's no way for users to know that they could get a 100x performance improvement by installing mss)
- add mss as a hard dependency (This would also be good as an end user, but if I'm understanding correctly, Pillow has a rule of "no hard dependencies"?)

(For context to why I want to do this - I've been looking for a fast cross-platform imagegrab library for years, but everywhere I looked the advice was "use Pillow or PyScreenshot"; both of those are slow, so I ended up writing my own python extension in C. MSS is amazing, but despite searching for it for years, I never found it. I think the world would be a better place if we combined the well-known-and-widely-used-ness of Pillow with the awesome implementation from MSS :) )
Thanks @shish !

IMO the option 1 is not ideal for either part. Code duplication will result in too many issues, patches not sent upstream or big code difference between implementations with time.

Option 2 may be good. It can let the user to not use MSS. A simple warning can be triggered if not installed (noisy but skipable by users).

I think the option 3 will be useless. Users will continue to have a slowly (and in many cases not working at all) version without notice. Most people will not be aware at all of the alternative way.

Option 4 is a good, but it is not my voice here :)

I can think of a 5th option: vendoring. PyPi is doing that and it seems to work pretty well. Issues and patches should be opened upstream and changes will automatically by available with the next Pillow release. The only drawback is that you can't have an up-to-date MSS version depending of the release process of Pillow. Using the option 4 will allow user to simple have the latest MSS version more quickly.

BTW thanks for compliments :)
As I 've tested , PyScreenshot is not slow on Linux , instead I felt it is really fast, so any consideration on incorporating the Linux part of PyScreenshot into this project ?
What is "fast" for you? PyScreenshot's own benchmarks in the README say that the fastest backend is ~340ms to take a screenshot, compared to ~5ms using native functions like mss does...

(But still, having imagegrab work on Linux would be nice for a lot of people, even if it doesn't work for real-time needs :P )

Resolving patch:
diff --git a/setup.py b/setup.py
--- a/setup.py
+++ b/setup.py
@@ -286,6 +286,7 @@ class feature:
             "webpmux",
             "jpeg2000",
             "imagequant",
+            "xcb",
         ]
 
         required = {"jpeg", "zlib"}
@@ -681,6 +682,12 @@ def build_extensions(self):
                 ):
                     feature.webpmux = "libwebpmux"
 
+        if feature.want("xcb"):
+            _dbg("Looking for xcb")
+            if _find_include_file(self, "xcb/xcb.h"):
+                if _find_library_file(self, "xcb"):
+                    feature.xcb = "xcb"
+
         for f in feature:
             if not getattr(feature, f) and feature.require(f):
                 if f in ("jpeg", "zlib"):
@@ -715,6 +722,9 @@ def build_extensions(self):
         if feature.tiff:
             libs.append(feature.tiff)
             defs.append(("HAVE_LIBTIFF", None))
+        if feature.xcb:
+            libs.append(feature.xcb)
+            defs.append(("HAVE_XCB", None))
         if sys.platform == "win32":
             libs.extend(["kernel32", "user32", "gdi32"])
         if struct.unpack("h", b"\0\1")[0] == 1:
@@ -813,6 +823,7 @@ def summary_report(self, feature):
             (feature.lcms, "LITTLECMS2"),
             (feature.webp, "WEBP"),
             (feature.webpmux, "WEBPMUX"),
+            (feature.xcb, "XCB (X protocol)"),
         ]
 
         all = 1
diff --git a/src/PIL/ImageGrab.py b/src/PIL/ImageGrab.py
--- a/src/PIL/ImageGrab.py
+++ b/src/PIL/ImageGrab.py
@@ -15,45 +15,56 @@
 # See the README file for information on usage and redistribution.
 #
 
-import os
-import subprocess
 import sys
-import tempfile
 
 from . import Image
 
-if sys.platform not in ["win32", "darwin"]:
-    raise ImportError("ImageGrab is macOS and Windows only")
+if sys.platform == "darwin":
+    import os
+    import tempfile
+    import subprocess
 
 
-def grab(bbox=None, include_layered_windows=False, all_screens=False):
-    if sys.platform == "darwin":
-        fh, filepath = tempfile.mkstemp(".png")
-        os.close(fh)
-        subprocess.call(["screencapture", "-x", filepath])
-        im = Image.open(filepath)
-        im.load()
-        os.unlink(filepath)
-        if bbox:
-            im_cropped = im.crop(bbox)
-            im.close()
-            return im_cropped
-    else:
-        offset, size, data = Image.core.grabscreen(include_layered_windows, all_screens)
-        im = Image.frombytes(
-            "RGB",
-            size,
-            data,
-            # RGB, 32-bit line padding, origin lower left corner
-            "raw",
-            "BGR",
-            (size[0] * 3 + 3) & -4,
-            -1,
-        )
-        if bbox:
-            x0, y0 = offset
-            left, top, right, bottom = bbox
-            im = im.crop((left - x0, top - y0, right - x0, bottom - y0))
+def grab(bbox=None, include_layered_windows=False, all_screens=False, xdisplay=None):
+    if xdisplay is None:
+        if sys.platform == "darwin":
+            fh, filepath = tempfile.mkstemp(".png")
+            os.close(fh)
+            subprocess.call(["screencapture", "-x", filepath])
+            im = Image.open(filepath)
+            im.load()
+            os.unlink(filepath)
+            if bbox:
+                im_cropped = im.crop(bbox)
+                im.close()
+                return im_cropped
+            return im
+        elif sys.platform == "win32":
+            offset, size, data = Image.core.grabscreen_win32(
+                include_layered_windows, all_screens
+            )
+            im = Image.frombytes(
+                "RGB",
+                size,
+                data,
+                # RGB, 32-bit line padding, origin lower left corner
+                "raw",
+                "BGR",
+                (size[0] * 3 + 3) & -4,
+                -1,
+            )
+            if bbox:
+                x0, y0 = offset
+                left, top, right, bottom = bbox
+                im = im.crop((left - x0, top - y0, right - x0, bottom - y0))
+            return im
+    # use xdisplay=None for default display on non-win32/macOS systems
+    if not Image.core.HAVE_XCB:
+        raise IOError("Pillow was built without XCB support")
+    size, data = Image.core.grabscreen_x11(xdisplay)
+    im = Image.frombytes("RGB", size, data, "raw", "BGRX", size[0] * 4, 1)
+    if bbox:
+        im = im.crop(bbox)
     return im
 
 
@@ -81,11 +92,13 @@ def grabclipboard():
             im.load()
         os.unlink(filepath)
         return im
-    else:
-        data = Image.core.grabclipboard()
+    elif sys.platform == "win32":
+        data = Image.core.grabclipboard_win32()
         if isinstance(data, bytes):
             from . import BmpImagePlugin
             import io
 
             return BmpImagePlugin.DibImageFile(io.BytesIO(data))
         return data
+    else:
+        raise NotImplementedError("ImageGrab.grabclipboard() is macOS and Windows only")
diff --git a/src/PIL/features.py b/src/PIL/features.py
--- a/src/PIL/features.py
+++ b/src/PIL/features.py
@@ -56,6 +56,7 @@ def get_supported_codecs():
     "raqm": ("PIL._imagingft", "HAVE_RAQM"),
     "libjpeg_turbo": ("PIL._imaging", "HAVE_LIBJPEGTURBO"),
     "libimagequant": ("PIL._imaging", "HAVE_LIBIMAGEQUANT"),
+    "xcb": ("PIL._imaging", "HAVE_XCB"),
 }
 
 
@@ -132,6 +133,7 @@ def pilinfo(out=None, supported_formats=True):
         ("libtiff", "LIBTIFF"),
         ("raqm", "RAQM (Bidirectional Text)"),
         ("libimagequant", "LIBIMAGEQUANT (Quantization method)"),
+        ("xcb", "XCB (X protocol)"),
     ]:
         if check(name):
             print("---", feature, "support ok", file=out)


Test patch:
diff --git a/Tests/test_imagegrab.py b/Tests/test_imagegrab.py
--- a/Tests/test_imagegrab.py
+++ b/Tests/test_imagegrab.py
@@ -2,67 +2,72 @@
 import sys
 
 import pytest
+from PIL import Image, ImageGrab
 
 from .helper import assert_image
 
-try:
-    from PIL import ImageGrab
 
-    class TestImageGrab:
-        def test_grab(self):
-            for im in [
-                ImageGrab.grab(),
-                ImageGrab.grab(include_layered_windows=True),
-                ImageGrab.grab(all_screens=True),
-            ]:
-                assert_image(im, im.mode, im.size)
-
-            im = ImageGrab.grab(bbox=(10, 20, 50, 80))
-            assert_image(im, im.mode, (40, 60))
-
-        def test_grabclipboard(self):
-            if sys.platform == "darwin":
-                subprocess.call(["screencapture", "-cx"])
-            else:
-                p = subprocess.Popen(
-                    ["powershell", "-command", "-"], stdin=subprocess.PIPE
-                )
-                p.stdin.write(
-                    b"""[Reflection.Assembly]::LoadWithPartialName("System.Drawing")
-[Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms")
-$bmp = New-Object Drawing.Bitmap 200, 200
-[Windows.Forms.Clipboard]::SetImage($bmp)"""
-                )
-                p.communicate()
-
-            im = ImageGrab.grabclipboard()
+class TestImageGrab:
+    @pytest.mark.skipif(
+        sys.platform not in ("win32", "darwin"), reason="requires Windows or macOS"
+    )
+    def test_grab(self):
+        for im in [
+            ImageGrab.grab(),
+            ImageGrab.grab(include_layered_windows=True),
+            ImageGrab.grab(all_screens=True),
+        ]:
             assert_image(im, im.mode, im.size)
 
+        im = ImageGrab.grab(bbox=(10, 20, 50, 80))
+        assert_image(im, im.mode, (40, 60))
 
-except ImportError:
-
-    class TestImageGrab:
-        @pytest.mark.skip(reason="ImageGrab ImportError")
-        def test_skip(self):
-            pass
+    @pytest.mark.skipif(not Image.core.HAVE_XCB, reason="requires XCB")
+    def test_grab_x11(self):
+        try:
+            if sys.platform not in ("win32", "darwin"):
+                im = ImageGrab.grab()
+                assert_image(im, im.mode, im.size)
 
+            im2 = ImageGrab.grab(xdisplay="")
+            assert_image(im2, im2.mode, im2.size)
+        except IOError as e:
+            pytest.skip(str(e))
 
-class TestImageGrabImport:
-    def test_import(self):
-        # Arrange
-        exception = None
+    @pytest.mark.skipif(Image.core.HAVE_XCB, reason="tests missing XCB")
+    def test_grab_no_xcb(self):
+        if sys.platform not in ("win32", "darwin"):
+            with pytest.raises(IOError) as e:
+                ImageGrab.grab()
+            assert str(e.value).startswith("Pillow was built without XCB support")
 
-        # Act
-        try:
-            from PIL import ImageGrab
+        with pytest.raises(IOError) as e:
+            ImageGrab.grab(xdisplay="")
+        assert str(e.value).startswith("Pillow was built without XCB support")
 
-            ImageGrab.__name__  # dummy to prevent Pyflakes warning
-        except Exception as e:
-            exception = e
+    @pytest.mark.skipif(not Image.core.HAVE_XCB, reason="requires XCB")
+    def test_grab_invalid_xdisplay(self):
+        with pytest.raises(IOError) as e:
+            ImageGrab.grab(xdisplay="error.test:0.0")
+        assert str(e.value).startswith("X connection failed")
 
-        # Assert
-        if sys.platform in ["win32", "darwin"]:
-            assert exception is None
+    def test_grabclipboard(self):
+        if sys.platform == "darwin":
+            subprocess.call(["screencapture", "-cx"])
+        elif sys.platform == "win32":
+            p = subprocess.Popen(["powershell", "-command", "-"], stdin=subprocess.PIPE)
+            p.stdin.write(
+                b"""[Reflection.Assembly]::LoadWithPartialName("System.Drawing")
+[Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms")
+$bmp = New-Object Drawing.Bitmap 200, 200
+[Windows.Forms.Clipboard]::SetImage($bmp)"""
+            )
+            p.communicate()
         else:
-            assert isinstance(exception, ImportError)
-            assert str(exception) == "ImageGrab is macOS and Windows only"
+            with pytest.raises(NotImplementedError) as e:
+                ImageGrab.grabclipboard()
+            assert str(e.value) == "ImageGrab.grabclipboard() is macOS and Windows only"
+            return
+
+        im = ImageGrab.grabclipboard()
+        assert_image(im, im.mode, im.size)