pyautoguiのlocateOnScreen()がマルチディスプレイ非対応な理由

## 概要
pyautoguiのlocateOnScreen()がデュアルディスプレイ環境にも関わらず一画面分でしか探索ができなかったのでわかったところまでを備忘録として記します。

## 結論
PILのimageGrab.grab()がデュアルディスプレイに対応していない
pyautgiuのマルチモニター非対応の解決策を書いている人がいました(未検証)→https://github.com/python-pillow/Pillow/issues/1547#issuecomment-185815425

## 問題発生時の環境
Winodws10 home
python 3.6.4
pyautogui 0.9.50
pyscreeze 0.1.26
Pillow 5.0.0


## pyautogui.locateOnScreen()の動作
この関数はpayautoguiの`__init__.py`の212行目で以下のように定義されています。

```payautogui/__init__.py
 def locateOnScreen(*args, **kwargs):
        return pyscreeze.locateOnScreen(*args, **kwargs)
```
つまりこの関数は別のモジュールの関数をそのまま引っ張ってきているだけでした。
そして、この pyscreeze.locateOnScreen()は`pyscreeze/__init__.py`の350行目に以下のように定義されています。

```pyscreeze/__init__.py
def locateOnScreen(image, minSearchTime=0, **kwargs):
    """TODO - rewrite this
    minSearchTime - amount of time in seconds to repeat taking
    screenshots and trying to locate a match.  The default of 0 performs
    a single search.
    """
    start = time.time()
    while True:
        try:
            screenshotIm = screenshot(region=None) # the locateAll() function must handle cropping to return accurate coordinates, so don't pass a region here.
            retVal = locate(image, screenshotIm, **kwargs)
            try:
                screenshotIm.fp.close()
            except AttributeError:
                # Screenshots on Windows won't have an fp since they came from
                # ImageGrab, not a file. Screenshots on Linux will have fp set
                # to None since the file has been unlinked
                pass
            if retVal or time.time() - start > minSearchTime:
                return retVal
        except ImageNotFoundException:
            if time.time() - start > minSearchTime:
                if USE_IMAGE_NOT_FOUND_EXCEPTION:
                    raise
                else:
                    return None
```
まずはじめに`screenshot(region=None)`でスクリーンショットを撮り、その中から`retVal = locate(image, screenshotIm, **kwargs)`で与えられた画像との一致箇所を探しているようです。
ということは、このscreenshot関数が一画面分しか撮れていなければマルチディスプレイ全体で探し出すことはできません。
このscreenshot関数は同ファイルの591行目にあるようにプラットフォーム毎に関数が変わる仕組みになっていて、windowsでは`screenshot = _screenshot_win32`となっています。これは同ファイル421行目にあり、

```pyscreeze/__init__.py
def _screenshot_win32(imageFilename=None, region=None):
    """
    TODO
    """
    # TODO - Use the winapi to get a screenshot, and compare performance with ImageGrab.grab()
    # https://stackoverflow.com/a/3586280/1893164
    im = ImageGrab.grab()
    if region is not None:
        assert len(region) == 4, 'region argument must be a tuple of four ints'
        region = [int(x) for x in region]
        im = im.crop((region[0], region[1], region[2] + region[0], region[3] + region[1]))
    if imageFilename is not None:
        im.save(imageFilename)
    return im

```
となっていて、im = ImageGrab.grab()でスクリーンショットを撮ってそれを返しています。つまり、pyautoguiのlocateOnScreen()がマルチディスプレイに対応していないのは、pillowの`ImageGran.grab()`がプライマリモニターしかスクショを撮れないところに起因しています。
調べているとちょうど同じ問題を解決した(しかもpyautogui!)方をみつけました。
https://github.com/python-pillow/Pillow/issues/1547#issuecomment-185815425

解決できそうなので、ついでに何故`ImageGrab.grab()`がマルチディスプレイに対応していないかを見てみたいと思います。
この関数はPIL/ImageGrab.pyの32行目以下に定義されています。

```PIL/ImageGrab.py
def grab(bbox=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)
    else:
        size, data = grabber()
        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:
        im = im.crop(bbox)
    return im
```
このようになっていて、windowsの場合、スクリーンショットの大きさは`grabber()`によって決められていることがわかります。このgrabber関数は同ファイル24行目にあり
`grabber = Image.core.grabscreen`
となっています。
しかし、自分はこのImage.core.grabberが見つからず心がしんどくなりました。

ちなみにここを[githubのリポジトリ](https://github.com/python-pillow/Pillow/blob/3bb39c73dd75f34c470c6ee1521f9bfa22162f5f/src/PIL/ImageGrab.py)で見てみると

```PIL/ImageGrab.py
offset, size, data = Image.core.grabscreen_win32(
                include_layered_windows, all_screens
            )
```
となっていました。pillowって最新版7.0.0越えてるんですね...
`grabscreen_win32`は[_imaging.c](https://github.com/python-pillow/Pillow/blob/3bb39c73dd75f34c470c6ee1521f9bfa22162f5f/src/_imaging.c)に

```_imaging.c
 {"grabscreen_win32", (PyCFunction)PyImaging_GrabScreenWin32, 1},
```
とかいてあるのを見つけました。これはどうもC/Python APIでの書き方だそうですが、C言語が読めないためよくわからず...
.

.

.


と思いきやこんなポストを発見。
https://gitmemory.com/issue/python-pillow/Pillow/1547/499147024
これには`PyImaging_GrabScreenWin32()`は`display.c`にあるとあります。
さっそく見てみると[323行目](PyImaging_GrabScreenWin32(PyObject* self, PyObject* args))にありました。
また、このポストによるとこの関数内で使われている`GetDeviceCaps`がプライマリモニターのピクセルサイズしか返さないとのこと。調べていると、この関数はどうやらwindowsのAPIらしく、[ドキュメント](https://docs.microsoft.com/en-us/windows/win32/api/wingdi/nf-wingdi-getdevicecaps)がありました。
`display.c`の337行目の

```Pillow/display.c
width = GetDeviceCaps(screen, HORZRES);
height = GetDeviceCaps(screen, VERTRES);
```
がディスプレイのサイズを受け取っている所のようなので、もしかしてここをオラオラで変えてやればいけるのでは？というのは明日以降にします(今日はバイトがあるので)

と思ったもののDisplay.cが見つけられませんでした。なんで.....?
また進展があり次第追記・編集します。