Repository: bokeh/bokeh

Issue:
Export fails on OSX
As I noted in the issue about switching to chrome as the default, a long time ago, the default settings do not work on OSX because they use the wrong pixel ratio:

![iris](https://user-images.githubusercontent.com/1078448/74006088-80b72c00-492f-11ea-9e5f-4715ddc80b74.png)



Discussion:
I guess a bigger question is: why are these tests passing on the macos builds on GH CI? Becasue all the chromium export tests fail locally for me.
Here is the relevant issue (that was not referenced or closed by the PR for some reason) https://github.com/bokeh/bokeh/issues/8176
Tho, I am guessing this is happening with FF (so, both) if that is the default? 

BTW ` /Applications/Google\ Chrome.app/Contents/MacOS/` is not going to be on anyone's PATH. You will need to search for `Google Chrome` at that absolute location. 
There are explicit tests for both browsers, i.e. Chromium/Chrome and Firefox, so there is no way you will get let's say Firefox tested twice, though how exactly those tests will fail is a different matter. There is also a default test, so if Chromium/Chrome can't be found on your system, you will be notified. In general, I need test output to have a chance at understanding what's going on, especially it looks like there are multiple issues happening at the same time.
The tests didn't catch the difference in pixel ratio, because they disable hidpi transform to have any chance at working across platforms, especially within the confines of unit testing. It's also not clear to me what to expect in terms of pixel ratio on headless systems. We are currently not set up at all to catch such issues. Within the recent work on bokehjs testing, I've been experimenting with performing testing with explicit device metrics (resolution, pixel density, etc.) with chrome's devtools protocol. Though that would have to be generalized, to make it available for io.export testing.
Here is one example: 
```
______________________________________________ test_get_screenshot_as_png[chromium-dimensions0] _______________________________________________

webdriver = <selenium.webdriver.chrome.webdriver.WebDriver (session="a19bce7f4d935c45801481e2b875e518")>, dimensions = (14, 14)

    @pytest.mark.selenium
    @pytest.mark.parametrize("dimensions", [(14, 14), (44, 44), (144, 144), (444, 444), (1444, 1444)])
    def test_get_screenshot_as_png(webdriver, dimensions: Tuple[int, int]) -> None:
        width, height = dimensions
        border = 5

        layout = Plot(x_range=Range1d(), y_range=Range1d(),
                      plot_height=width, plot_width=height,
                      min_border=border,
                      hidpi=False,
                      toolbar_location=None,
                      outline_line_color=None, background_fill_color="#00ff00", border_fill_color="#00ff00")

        with silenced(MISSING_RENDERERS):
            png = bie.get_screenshot_as_png(layout, driver=webdriver)

        # a WxHpx image of white pixels
        assert png.size == (width, height)

        data = png.tobytes()
        assert len(data) == 4*width*height
>       assert (data == b"\x00\xff\x00\xff"*width*height or
                data == b"\x2e\xff\x05\xff"*width*height)   # XXX: Chrome on MacOS
E       AssertionError: assert (b'u\xfaL\xffu...xffu\xfaL\xff' == b'\x00\xff\x0...0\xff\x00\xff'
E         At index 0 diff: b'u' != b'\x00'
E         Use -v to get the full diff or b'u\xfaL\xffu...xffu\xfaL\xff' == b'.\xff\x05\x....\xff\x05\xff'
E         At index 0 diff: b'u' != b'.'
E         Use -v to get the full diff)
```

Unfortunately turning on -vv (or even -v) hangs the test.

Offhand, my guess is that if HiDPI is disabled on GH CI then the result is matching the first clause of the "or" data values (i.e. full proper image, but scaled 1/4 smaller), in contrast to the image generated on a real laptop which is scaled 4x bigger but then cropped to only have 1/4 of it showing. 

Also FYI there is some flakiness in using chromium an CI, just restarted a test with this
> E       selenium.common.exceptions.WebDriverException: Message: unknown error: Chrome failed to start: exited abnormally
E         (chrome not reachable)
E         (The process started from chrome location /usr/bin/google-chrome is no longer running, so ChromeDriver is assuming that Chrome has crashed.)
>  selenium.common.exceptions.WebDriverException: Message: unknown error: Chrome failed to start: exited abnormally

I saw this in some PRs as well. I presume it's selenium or the driver having an issue, because otherwise I didn't have a single problem with starting chrome/chromium when doing things outside of selenium.

Resolving patch:
diff --git a/bokeh/io/export.py b/bokeh/io/export.py
--- a/bokeh/io/export.py
+++ b/bokeh/io/export.py
@@ -198,10 +198,13 @@ def get_screenshot_as_png(obj: Union[LayoutDOM, Document], *, driver: Optional[W
         web_driver.maximize_window()
         web_driver.get("file:///" + tmp.path)
         wait_until_render_complete(web_driver, timeout)
-        [width, height] = _maximize_viewport(web_driver)
+        [width, height, dpr] = _maximize_viewport(web_driver)
         png = web_driver.get_screenshot_as_png()
 
-    return Image.open(io.BytesIO(png)).convert("RGBA").crop((0, 0, width, height))
+    return (Image.open(io.BytesIO(png))
+                 .convert("RGBA")
+                 .crop((0, 0, width*dpr, height*dpr))
+                 .resize((width, height)))
 
 def get_svgs(obj: Union[LayoutDOM, Document], *, driver: Optional[WebDriver] = None, timeout: int = 5,
         resources: Resources = INLINE, width: Optional[int] = None, height: Optional[int] = None) -> List[str]:
@@ -316,19 +319,19 @@ def _log_console(driver: WebDriver) -> None:
         for message in messages:
             log.warning(message)
 
-def _maximize_viewport(web_driver: WebDriver) -> Tuple[int, int]:
+def _maximize_viewport(web_driver: WebDriver) -> Tuple[int, int, int]:
     calculate_viewport_size = """\
         const root = document.getElementsByClassName("bk-root")[0]
         const {width, height} = root.children[0].getBoundingClientRect()
-        return [width, height]
+        return [width, height, window.devicePixelRatio]
     """
-    viewport_size: Tuple[int, int] = web_driver.execute_script(calculate_viewport_size)
+    viewport_size: Tuple[int, int, int] = web_driver.execute_script(calculate_viewport_size)
     calculate_window_size = """\
-        const [width, height] = arguments
+        const [width, height, dpr] = arguments
         return [
             // XXX: outer{Width,Height} can be 0 in headless mode under certain window managers
-            Math.max(0, window.outerWidth - window.innerWidth) + width,
-            Math.max(0, window.outerHeight - window.innerHeight) + height,
+            Math.max(0, window.outerWidth - window.innerWidth) + width*dpr,
+            Math.max(0, window.outerHeight - window.innerHeight) + height*dpr,
         ]
     """
     [width, height] = web_driver.execute_script(calculate_window_size, *viewport_size)
diff --git a/bokeh/io/webdriver.py b/bokeh/io/webdriver.py
--- a/bokeh/io/webdriver.py
+++ b/bokeh/io/webdriver.py
@@ -63,6 +63,7 @@ def create_chromium_webdriver() -> WebDriver:
     options = webdriver.chrome.options.Options()
     options.add_argument("--headless")
     options.add_argument("--hide-scrollbars")
+    options.add_argument("--force-device-scale-factor=1")
     return webdriver.Chrome(options=options)
 
 #-----------------------------------------------------------------------------


Test patch: