Repository: python-pillow/Pillow

Issue:
ImageGrab fails with multiple monitors
When calling ImageGrab.grab() passing in a bounding box that is outside the area of my primary monitor, I just get black.

For example, my primary monitor is 1920x1200, flanked on either side by monitors running at 1600x1200, making my total desktop size 5120x1200. Also, because my primary monitor is in the middle, the horizontal coordinates for the full virtual desktop go from -1600 to 3519, where 0 is the left-most pixel of my primary monitor. If I try to capture my rightmost monitor using the following code, all I get is a black image:

``` Python
from PIL import ImageGrab
img = ImageGrab.grab([1920, 0, 3519, 1199])
img.save("test.jpg")
```

Poking around the code, it looks like `ImageGrab.grab()` calls into `Image.core.grabscreen` which is an alias for [`PyImaging_GrabScreenWin32()`](https://github.com/python-pillow/Pillow/blob/2be12dec2b231d31400f44bfa855966484997c16/display.c#L323) in `display.c`. That function does retrieve a DC handle to the entire desktop, but the subsequent calls to `GetDeviceCaps` with `HORZRES` and `VERTRES` only return the x/y size of the primary monitor, not the entire desktop.

``` C
screen = CreateDC("DISPLAY", NULL, NULL, NULL);
// ...
width = GetDeviceCaps(screen, HORZRES);
height = GetDeviceCaps(screen, VERTRES);
// ...
if (!BitBlt(screen_copy, 0, 0, width, height, screen, 0, 0, SRCCOPY))
    goto error;
```

Another problem with the above code is that monitors to the left of or above the primary display have negative coordinates in the `screen` DC. So, for example, capturing the monitor to the left of my primary display (which has a resolution of 1600x1200) would need to call BitBlt with the following coordinates:

``` C
left = -1600
top = 0
width = 1600
height = 1200
BitBlt(screen_copy, 0, 0, width, height, screen, left, top, SRCCOPY)
```

Similarly, if I was trying to capture a monitor above my primary display, then `top` would be negative. Because of the negative coordinates issue, I don't see any way of fixing this without passing in `left, top, width, height` from the calling python code, which could be calculated easily from the `bbox` parameter. Then it's simply up to the caller to know the coordinates of the monitor they want to capture. If no `bbox` is provided, then the coordinates would default to the primary display (0, 0, HORZRES, VERTRES), keeping the current functionality unchanged so as not to break existing code that uses `ImageGrab.grab()`.



Discussion:
Does the solution using pywin32 at http://stackoverflow.com/a/4589290/453463 work for you?

@cgohlke Thanks, but that solution doesn't work for what I'm trying to do, or at least not without a lot of convoluted code to translate between coordinate systems. The code on your stackoverflow answer grabs the entire screen, whereas I only want to grab a single monitor at a time. If there are monitors to the left were above the primary monitor, the coordinates of the monitors returned by GetMonitorInfo are completely different from the coordinates of the grabbed image of the virtual desktop.

What I ended up doing was essentially to translate the PyImaging_GrabScreenWin32 C function to Python using ctypes, incorporating the changes I suggested above. I've been using it since last night and so far it seems to be working well. I mainly opened this issue to suggest a way that Pillow could be extended to support this and similar scenarios, without breaking existing code.

Here's the code I ended up with, in case it might help someone else:

``` Python
from ctypes import windll, c_int, c_uint, c_char_p, c_buffer
from struct import calcsize, pack
from PIL import Image

gdi32 = windll.gdi32

# Win32 functions
CreateDC = gdi32.CreateDCA
CreateCompatibleDC = gdi32.CreateCompatibleDC
GetDeviceCaps = gdi32.GetDeviceCaps
CreateCompatibleBitmap = gdi32.CreateCompatibleBitmap
BitBlt = gdi32.BitBlt
SelectObject = gdi32.SelectObject
GetDIBits = gdi32.GetDIBits
DeleteDC = gdi32.DeleteDC
DeleteObject = gdi32.DeleteObject

# Win32 constants
NULL = 0
HORZRES = 8
VERTRES = 10
SRCCOPY = 13369376
HGDI_ERROR = 4294967295
ERROR_INVALID_PARAMETER = 87

def grab_screen(bbox=None):
    """
    Grabs a screenshot. This is a replacement for PIL's ImageGrag.grab() method
    that supports multiple monitors. (SEE: https://github.com/python-pillow/Pillow/issues/1547)

    Returns a PIL Image, so PIL library must be installed.

    Usage:
        im = grab_screen() # grabs a screenshot of the primary monitor
        im = grab_screen([-1600, 0, -1, 1199]) # grabs a 1600 x 1200 screenshot to the left of the primary monitor
        im.save('screencap.jpg')
    """

    def cleanup():
        if bitmap:
            DeleteObject(bitmap)
        DeleteDC(screen_copy)
        DeleteDC(screen)

    try:
        screen = CreateDC(c_char_p('DISPLAY'), NULL, NULL, NULL)
        screen_copy = CreateCompatibleDC(screen)

        if bbox:
            left,top,x2,y2 = bbox
            width = x2 - left + 1
            height = y2 - top + 1
        else:
            left = 0
            top = 0
            width = GetDeviceCaps(screen, HORZRES)
            height = GetDeviceCaps(screen, VERTRES)

        bitmap = CreateCompatibleBitmap(screen, width, height)
        if bitmap == NULL:
            print('grab_screen: Error calling CreateCompatibleBitmap. Returned NULL')
            return

        hobj = SelectObject(screen_copy, bitmap)
        if hobj == NULL or hobj == HGDI_ERROR:
            print('grab_screen: Error calling SelectObject. Returned {0}.'.format(hobj))
            return

        if BitBlt(screen_copy, 0, 0, width, height, screen, left, top, SRCCOPY) == NULL:
            print('grab_screen: Error calling BitBlt. Returned NULL.')
            return

        bitmap_header = pack('LHHHH', calcsize('LHHHH'), width, height, 1, 24)
        bitmap_buffer = c_buffer(bitmap_header)
        bitmap_bits = c_buffer(' ' * (height * ((width * 3 + 3) & -4)))
        got_bits = GetDIBits(screen_copy, bitmap, 0, height, bitmap_bits, bitmap_buffer, 0)
        if got_bits == NULL or got_bits == ERROR_INVALID_PARAMETER:
            print('grab_screen: Error calling GetDIBits. Returned {0}.'.format(got_bits))
            return

        image = Image.frombuffer('RGB', (width, height), bitmap_bits, 'raw', 'BGR', (width * 3 + 3) & -4, -1)
        return image
    finally:
        cleanup()
```

And for reference, [here](https://github.com/synkarius/caster/issues/116) is a link to the issue in Caster that I'm trying to solve. This is part of a voice control package for accessibility. It takes a screenshot of the monitor, processes it looking for text that the user might want to click on, and then overlays numbered boxes which the user can speak to move the mouse to that location.

> Here's the code I ended up with, in case it might help someone else:

You're a hero to me. 

PyAutoGUI also uses Pillow's ImageGrab, which means that it can't find things outside of the primary monitor. In your spirit of helping the next guy, here is how to fix PyAutoGUI:

Save this code to "chilimangos.py":

``` python
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
#  chilimangoes.py
#  
#  https://github.com/python-pillow/Pillow/issues/1547
#  

from ctypes import windll, c_int, c_uint, c_char_p, c_buffer
from struct import calcsize, pack
from PIL import Image

gdi32 = windll.gdi32

# Win32 functions
CreateDC = gdi32.CreateDCA
CreateCompatibleDC = gdi32.CreateCompatibleDC
GetDeviceCaps = gdi32.GetDeviceCaps
CreateCompatibleBitmap = gdi32.CreateCompatibleBitmap
BitBlt = gdi32.BitBlt
SelectObject = gdi32.SelectObject
GetDIBits = gdi32.GetDIBits
DeleteDC = gdi32.DeleteDC
DeleteObject = gdi32.DeleteObject

# Win32 constants
NULL = 0
HORZRES = 8
VERTRES = 10
SRCCOPY = 13369376
HGDI_ERROR = 4294967295
ERROR_INVALID_PARAMETER = 87

#from http://www.math.uiuc.edu/~gfrancis/illimath/windows/aszgard_mini/movpy-2.0.0-py2.4.4/movpy/lib/win32/lib/win32con.py
SM_XVIRTUALSCREEN = 76
SM_YVIRTUALSCREEN = 77
SM_CXVIRTUALSCREEN = 78
SM_CYVIRTUALSCREEN = 79
SM_CMONITORS = 80

def grab_screen(region=None):
    """
    Grabs a screenshot. This is a replacement for PIL's ImageGrag.grab() method
    that supports multiple monitors. (SEE: https://github.com/python-pillow/Pillow/issues/1547)

    Returns a PIL Image, so PIL library must be installed.

    Usage:
        im = grab_screen() # grabs a screenshot of the primary monitor
        im = grab_screen([-1600, 0, -1, 1199]) # grabs a 1600 x 1200 screenshot to the left of the primary monitor
        im.save('screencap.jpg')
    """
    bitmap = None
    try:
        screen = CreateDC(c_char_p('DISPLAY'),NULL,NULL,NULL)
        screen_copy = CreateCompatibleDC(screen)

        if region:
            left,top,x2,y2 = region
            width = x2 - left + 1
            height = y2 - top + 1
        else:
            left = windll.user32.GetSystemMetrics(SM_XVIRTUALSCREEN)
            top = windll.user32.GetSystemMetrics(SM_YVIRTUALSCREEN)
            width = windll.user32.GetSystemMetrics(SM_CXVIRTUALSCREEN)
            height = windll.user32.GetSystemMetrics(SM_CYVIRTUALSCREEN)

        bitmap = CreateCompatibleBitmap(screen, width, height)
        if bitmap == NULL:
            print('grab_screen: Error calling CreateCompatibleBitmap. Returned NULL')
            return

        hobj = SelectObject(screen_copy, bitmap)
        if hobj == NULL or hobj == HGDI_ERROR:
            print('grab_screen: Error calling SelectObject. Returned {0}.'.format(hobj))
            return

        if BitBlt(screen_copy, 0, 0, width, height, screen, left, top, SRCCOPY) == NULL:
            print('grab_screen: Error calling BitBlt. Returned NULL.')
            return

        bitmap_header = pack('LHHHH', calcsize('LHHHH'), width, height, 1, 24)
        bitmap_buffer = c_buffer(bitmap_header)
        bitmap_bits = c_buffer(' ' * (height * ((width * 3 + 3) & -4)))
        got_bits = GetDIBits(screen_copy, bitmap, 0, height, bitmap_bits, bitmap_buffer, 0)
        if got_bits == NULL or got_bits == ERROR_INVALID_PARAMETER:
            print('grab_screen: Error calling GetDIBits. Returned {0}.'.format(got_bits))
            return

        image = Image.frombuffer('RGB', (width, height), bitmap_bits, 'raw', 'BGR', (width * 3 + 3) & -4, -1)
        return image
    finally:
        if bitmap is not None:
            if bitmap:
                DeleteObject(bitmap)
            DeleteDC(screen_copy)
            DeleteDC(screen)
```

Then your pyautogui import needs to look like this:

``` python
import pyautogui
import chilimangoes
pyautogui.screenshot = chilimangoes.grab_screen
pyautogui.pyscreeze.screenshot = chilimangoes.grab_screen
```

Works like a charm! Thanks Again.

Edited to provide more helpful error messages. 

@jonathanimb 
@chilimangoes 
Hey, you seem to have a solution for the exact issue I am facing! I did exactly what you said however I am getting this:

 `Traceback (most recent call last):
  File "C:/Python27/Lib/site-packages/PIL/screen.py", line 9, in <module>
    im2 = pyautogui.screenshot('my_screenshot.png')
  File "C:\Python27\Lib\site-packages\PIL\chilimangos.py", line 92, in grab_screen
UnboundLocalError: local variable 'bitmap' referenced before assignment
`

I hope this is clear, I am really new to Python and programming in general I hope you will help me out here! Thanks!
@snapsnapsnapsnap what Pillow version?
@aclark4life 

I believe its 4.0.0
@snapsnapsnapsnap 

That's probably an error with the windll, but the way I wrote the code provides a very unhelpful error. I edited the code to provide the correct error message. Try that and try googling the new error. If you need help with the error make sure to include the version of windows and the version of python you are using. 
@jonathanimb 

I am using windows 10, python 2.7 but as I mentioned, I am completely new and this is a little too much for me to handle. Is it possible to PM you to avoid useless spam?

I am currently getting 

`WindowsError: exception: access violation reading 0xFFFFFFFF00000000
`

I just want to automatize some stuff but this appears to be way more difficult than I thought it would be :))) Thanks in advance

Github does not do PM, but I can't help you with that anyway. I don't have Windows 10 and TBH I'm not much of a Windows guy anyway. Try reddit.com/r/learnpython. Make sure to include the full code, full error message (use github gist or pastebin), and windows version and python version. 
I've also just come across the error, it only appears to happen within a loop though. Just having line after line doesn't cause it to happen. A weird fix is just printing anything to the console, which seems to stop the error happening.

The code by chilimangoes only comes up with the error when putting it in a numpy array, but the one from jonathanimb appears even without numpy.

Edit: Found a quick fix, if the `CreateDC` line fails, catch the `WindowsError`, retry, and it'll work fine. Here it is if anyone else comes across this:

```python
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
#  chilimangoes.py
#  
#  https://github.com/python-pillow/Pillow/issues/1547
#  

from ctypes import windll, c_int, c_uint, c_char_p, c_buffer
from struct import calcsize, pack
from PIL import Image

gdi32 = windll.gdi32

# Win32 functions
CreateDC = gdi32.CreateDCA
CreateCompatibleDC = gdi32.CreateCompatibleDC
GetDeviceCaps = gdi32.GetDeviceCaps
CreateCompatibleBitmap = gdi32.CreateCompatibleBitmap
BitBlt = gdi32.BitBlt
SelectObject = gdi32.SelectObject
GetDIBits = gdi32.GetDIBits
DeleteDC = gdi32.DeleteDC
DeleteObject = gdi32.DeleteObject

# Win32 constants
NULL = 0
HORZRES = 8
VERTRES = 10
SRCCOPY = 13369376
HGDI_ERROR = 4294967295
ERROR_INVALID_PARAMETER = 87

#from http://www.math.uiuc.edu/~gfrancis/illimath/windows/aszgard_mini/movpy-2.0.0-py2.4.4/movpy/lib/win32/lib/win32con.py
SM_XVIRTUALSCREEN = 76
SM_YVIRTUALSCREEN = 77
SM_CXVIRTUALSCREEN = 78
SM_CYVIRTUALSCREEN = 79
SM_CMONITORS = 80

def grab_screen(region=None):
    """
    Grabs a screenshot. This is a replacement for PIL's ImageGrag.grab() method
    that supports multiple monitors. (SEE: https://github.com/python-pillow/Pillow/issues/1547)

    Returns a PIL Image, so PIL library must be installed.

    Usage:
        im = grab_screen() # grabs a screenshot of the primary monitor
        im = grab_screen([-1600, 0, -1, 1199]) # grabs a 1600 x 1200 screenshot to the left of the primary monitor
        im.save('screencap.jpg')
    """
    bitmap = None
    try:
        try:
            screen = CreateDC(c_char_p('DISPLAY'),NULL,NULL,NULL)
        except WindowsError:
            screen = CreateDC(c_char_p('DISPLAY'),NULL,NULL,NULL)
        screen_copy = CreateCompatibleDC(screen)

        if region:
            left,top,x2,y2 = region
            width = x2 - left + 1
            height = y2 - top + 1
        else:
            left = windll.user32.GetSystemMetrics(SM_XVIRTUALSCREEN)
            top = windll.user32.GetSystemMetrics(SM_YVIRTUALSCREEN)
            width = windll.user32.GetSystemMetrics(SM_CXVIRTUALSCREEN)
            height = windll.user32.GetSystemMetrics(SM_CYVIRTUALSCREEN)

        bitmap = CreateCompatibleBitmap(screen, width, height)
        if bitmap == NULL:
            print('grab_screen: Error calling CreateCompatibleBitmap. Returned NULL')
            return

        hobj = SelectObject(screen_copy, bitmap)
        if hobj == NULL or hobj == HGDI_ERROR:
            print('grab_screen: Error calling SelectObject. Returned {0}.'.format(hobj))
            return

        if BitBlt(screen_copy, 0, 0, width, height, screen, left, top, SRCCOPY) == NULL:
            print('grab_screen: Error calling BitBlt. Returned NULL.')
            return

        bitmap_header = pack('LHHHH', calcsize('LHHHH'), width, height, 1, 24)
        bitmap_buffer = c_buffer(bitmap_header)
        bitmap_bits = c_buffer(' ' * (height * ((width * 3 + 3) & -4)))
        got_bits = GetDIBits(screen_copy, bitmap, 0, height, bitmap_bits, bitmap_buffer, 0)
        if got_bits == NULL or got_bits == ERROR_INVALID_PARAMETER:
            print('grab_screen: Error calling GetDIBits. Returned {0}.'.format(got_bits))
            return

        image = Image.frombuffer('RGB', (width, height), bitmap_bits, 'raw', 'BGR', (width * 3 + 3) & -4, -1)
        return image
    finally:
        if bitmap is not None:
            if bitmap:
                DeleteObject(bitmap)
            DeleteDC(screen_copy)
            DeleteDC(screen)
```
@Peter92 When I try and run your fix, I get this exception:
```python
  File "C:\Users\SawyerPC\PycharmProjects\LIFX_Toys\gui.pyw", line 389, in eyedropper
    im = grab_screen()
  File "C:\Users\SawyerPC\PycharmProjects\LIFX_Toys\dual_monitors.py", line 56, in grab_screen
    screen = CreateDC(c_char_p('DISPLAY'),NULL,NULL,NULL)
TypeError: bytes or integer address expected instead of str instance
```
@samclane I take it the script without my fix runs fine on a single monitor? That particular error I think can be common if writing code with Python 2 and running it on 3, but in this case it's weird as I didn't touch anything related it. It's complaining at getting a `str` when it wants `bytes`. Maybe try change `c_char_p('DISPLAY')` to `bytes(c_char_p('DISPLAY'))`.
@Peter92 I've tried both @jonathanimb's fix and yours, both give me the same error. I've also tried your suggested `bytes()` fix, to no avail
```python
  File "C:\Users\SawyerPC\PycharmProjects\LIFX_Toys\gui.pyw", line 390, in eyedropper
    im = grab_screen()
  File "C:\Users\SawyerPC\PycharmProjects\LIFX_Toys\dual_monitors.py", line 55, in grab_screen
    screen = CreateDC(bytes(c_char_p('DISPLAY')),NULL,NULL,NULL)
TypeError: bytes or integer address expected instead of str instance
```

And it's not `NULL`s, those are ints. 
@samclane To be honest the original fix is pretty complex and I don't have a huge amount of experience with the windows API, there's only 2 other possibilities I can think of without someone rewriting it for Python 3.

1: Turn the text `'DISPLAY'` into bytes instead, maybe it's the `c_char_p` function which is failing.
2. Set `bytes(c_char_p('DISPLAY'))` to a variable, and use the variable inside `CreateDC`. Something similar to that caused an error for me previously since the API can be a bit weird.
@Peter92  Thanks for the help, but no dice. 

I found a module called [desktopmagic](https://pypi.org/project/Desktopmagic/) that addresses this issue, and seems to easily allow multimonitor screenshotting. It uses PIL, so if someone wanted to look through the source code and see how they do it, they might be able to get away with copying the code without importing the whole thing. But I'm just going to use the module for now.

Thanks again.
python2.7 vs 3.6 in ctypes   
         string 'DISPLAY'  need to be add "b" in front of string as Byte . try the code as below.
            screen = CreateDC(c_char_p(**b**'DISPLAY'),NULL,NULL,NULL)
        except WindowsError:
            screen = CreateDC(c_char_p(**b**'DISPLAY'),NULL,NULL,NULL)

PS: The Code Can run without error message . but 
    
    screen = np.array(ImageGrab.grab())  # capture at middle screen resolution at it's full res
                 // It's only 1 of 3 monitor image.



    screen = np.array(grab_screen())  # capture at middle screen resolution at it's full res
                // Nothing happen . Not sure any thing wrong for call this function ?                

This doesn't seem to work for me. I created the chilimangoes.py and added the b before the DISPLAY, but then I get the error:
`    left,top,x2,y2 = region
ValueError: too many values to unpack (expected 4)`
I discovered that its because with the chilimangoes code, it gets rid of the ability to input a file name for it to save to. Chilimangoes' version only asks for a 'region'. If I delete my filename portion, it will run, BUT nothing saves as it no longer is passed the output info... 

Is there some other way to output this to a file that I'm just over looking? I'm not too familiar with pyautogui nor pillow... so far, pyautogui was the only thing I could actually get to make a screenshot. . . But not being able to get capture multiple monitors is a huge hindrance...
> Is there some other way to output this to a file that I'm just over looking? 

It's right there in the usage instructions:

        im = grab_screen([-1600, 0, -1, 1199]) 
        im.save('screencap.jpg')

You could reduce it to one line if you wanted to: 

        grab_screen([-1600, 0, -1, 1199]).save('screencap.jpg')
> > Is there some other way to output this to a file that I'm just over looking?
> 
> It's right there in the usage instructions:
> 
> ```
>     im = grab_screen([-1600, 0, -1, 1199]) 
>     im.save('screencap.jpg')
> ```
> 
> You could reduce it to one line if you wanted to:
> 
> ```
>     grab_screen([-1600, 0, -1, 1199]).save('screencap.jpg')
> ```

-I'm sorry but I must be completely blind... I do not see that anywhere in the above postings...-
Edit: I see where it says it now, up in the top of the code's description. But it still doesn't work for me..

If I try to call `im = grab_screen([-1600, 0, -1, 1199])`

it tells me that grab_screen is not defined. 

Considering the previous posting for pyautogui said to add:
`auto.screenshot = chilimangoes.grab_screen`
`auto.pyscreeze.screenshot = chilimangoes.grab_screen`

(I had imported pyautogui as auto) it would appear that it is renaming the pyautogui's screenshot call to chilimangoes' grab_screen's call. 

When I look at the code that is posted for that, there is nothing in it for 'save'. So, if I do:
`im = auto.screenshot([-1920, 0, -1, 1080])`
`im.save('test.png')`
it just tells me that its finished with exit code 1... with no 'test.png' inside the local directory...

mind you, in my code, this is running inside of a loop, and this is instantly ending my while loop without displaying anything... not even the code to print things out immediately before the call to make a screen shot:

`        if click == 'Button.left':`
`            print(click)`
`            click = 'nom'`
`            print(click)`
`            count += 1`
`            print(count)`
`            print(auto.position())`
`            screenshot() `

So I'm lost... sorry... (the code to do the screenshotting is within the "screenshot()" function )
Running the debugger seems to show that its 'dying' because of a "TypeError(init)" within the "create_string_buffer" function inside of an __init__.py file somewhere... I don't really know anything about how the "C" stuff works, so idk what I'm really seeing here... Am I somehow passing a wrong character to the chilimangoes call and not knowing it???

Edit:
Ok... I think i get it... Because when you run the code unaltered, it throws the
`TypeError: bytes or integer address expected instead of str instance` 
error; us having added that "b" before the `'DISPLAY'` will fix that original TypeError, but then creates an unseen TypeError within whatever is doing the converting later on... This may just be a "well we're screwed, it can't be fixed" sort of thing from what I can tell... If you fix the first TypeError, it creates another TypeError, and I doubt we can just 'switch back' to the original 'Type' after the fact to correct it... 

Since the function (or whatever it is) needs to have the `c_char_p(b'DISPLAY')` done exactly in this way, switching it back somewhere else when `screen` is called won't actually accomplish anything...

So where is this `CreateDC` hiding at? And does it absolutely NEED to have something passed to it in the form of `c_char_p(b'DISPLAY')`???
@HikariNoKitsune I'm having a hard time understanding exactly what you're trying to do or where you're running into problems. But if all you need is to grab a screenshot, then I would take a look at the [Desktopmagic](https://github.com/ludios/Desktopmagic) library that @samclane mentioned above. I haven't used it myself, but it's a very small library and I looked over the source code and it's doing a very similar thing as my code above, but in a more robust manner.

To answer your question about CreateDC, that function is [part of the Win32 API](https://docs.microsoft.com/en-us/windows/desktop/api/wingdi/nf-wingdi-createdca). In other words, it's part of the core Windows DLLs. And yes, the first parameter does need to be passed in the form of `c_char_p(b'DISPLAY')`. If you look at the documentation for CreateDC, you'll see that the first three parameters are expected to be of type LPCSTR, which stands for "Long Pointer to a C String" (which is just a null–terminated byte array). The `c_char_p` function in the Python ctypes library takes care of translating the Python string into the pointer that CreateDC is expecting.
@chilimangoes ultimately what I am trying to do with my little program thing, is to take a screen shot every time the mouse is clicked. normally this works perfectly fine on its own, until there are more than one display. Of which, then I need it to take a screen shot of only the monitor that the mouse is currently 'on'. 

My issue comes up that whenever the mouse is 'clicked', I get the stated errors above about it expecting a byte and not a string. When that is corrected, and I use the code above geared towards pyautogui, the code will run, but nothing happens. It doesn't execute any of the actual code, but instead instantly quits. when I use PyCharm's debugger, it shows me that the `create_string_buffer` is throwing an error about expecting a string and NOT a byte. Thus, I'm stuck at a stand still with it because for the code to supposedly work, it needs to be a byte data type, but for the `create_string_buffer` it needs a normal string. So either the `c_char_p` is not working as its supposed to be, or something else is janked up...

Like I've said before, I've never really messed with the whole 'using C inside of Python' stuff before, so I have no clue about what in it does what...

But I'll look into the Desktopmagic library. It may be able to do what I've been trying to do for the past 3 days.

Resolving patch:
diff --git a/src/PIL/ImageGrab.py b/src/PIL/ImageGrab.py
--- a/src/PIL/ImageGrab.py
+++ b/src/PIL/ImageGrab.py
@@ -29,7 +29,7 @@
     raise ImportError("ImageGrab is macOS and Windows only")
 
 
-def grab(bbox=None, include_layered_windows=False):
+def grab(bbox=None, include_layered_windows=False, all_screens=False):
     if sys.platform == "darwin":
         fh, filepath = tempfile.mkstemp(".png")
         os.close(fh)
@@ -37,8 +37,10 @@ def grab(bbox=None, include_layered_windows=False):
         im = Image.open(filepath)
         im.load()
         os.unlink(filepath)
+        if bbox:
+            im = im.crop(bbox)
     else:
-        size, data = grabber(include_layered_windows)
+        offset, size, data = grabber(include_layered_windows, all_screens)
         im = Image.frombytes(
             "RGB",
             size,
@@ -49,8 +51,10 @@ def grab(bbox=None, include_layered_windows=False):
             (size[0] * 3 + 3) & -4,
             -1,
         )
-    if bbox:
-        im = im.crop(bbox)
+        if bbox:
+            x0, y0 = offset
+            left, top, right, bottom = bbox
+            im = im.crop((left - x0, top - y0, right - x0, bottom - y0))
     return im
 
 


Test patch:
diff --git a/Tests/test_imagegrab.py b/Tests/test_imagegrab.py
--- a/Tests/test_imagegrab.py
+++ b/Tests/test_imagegrab.py
@@ -8,7 +8,11 @@
 
     class TestImageGrab(PillowTestCase):
         def test_grab(self):
-            for im in [ImageGrab.grab(), ImageGrab.grab(include_layered_windows=True)]:
+            for im in [
+                ImageGrab.grab(),
+                ImageGrab.grab(include_layered_windows=True),
+                ImageGrab.grab(all_screens=True),
+            ]:
                 self.assert_image(im, im.mode, im.size)
 
         def test_grabclipboard(self):