Repository: napari/napari

Issue:
A 1-px border appears around images and is saved by viewer.screenshot()
## 🐛 Bug

A 1-px border appears around images and is saved by viewer.screenshot()

## To Reproduce

Steps to reproduce the behavior:
It's most visible in light mode:
1. Open any image.
2. Zoom in, so the image fills the canvas.
3. Note a 1-px light border—canvas?—around the image.
<img width="91" alt="image" src="https://user-images.githubusercontent.com/76622105/132459240-7f2f4b6b-6702-4672-b5fb-bf276d4e5492.png">

For dark mode it's not as visible in napari, but can be seen in screenshots:
1. Open Napari in `dark` theme
2. Open an image that has a light background, for example Samples>skimage>human_mitosis set `grey-r` colormap.
3. Take screenshot.
4. Note a 1-px dark border around the image.
<img width="77" alt="image" src="https://user-images.githubusercontent.com/76622105/132459830-53a7ede2-42b4-43a9-89b1-b88ce548843d.png">

## Expected behavior

No border pixels should be present. Or if the border is a feature, then it should be most definitive, few pixel border, not 1-px.

## Environment

 ```
napari: 0.4.11
Platform: macOS-11.5.2-arm64-arm-64bit
System: MacOS 11.5.2
Python: 3.9.6 | packaged by conda-forge | (default, Jul 6 2021, 08:51:19) [Clang 11.1.0 ]
Qt: 5.15.2
PyQt5: 5.15.4
NumPy: 1.21.0
SciPy: 1.7.0
Dask: 2021.06.2
VisPy: 0.7.0

OpenGL:
- GL version: 2.1 Metal - 71.7.1
- MAX_TEXTURE_SIZE: 16384

Screens:
- screen 1: resolution 1680x1050, scale 2.0

Plugins:
- 3 Utilities: 0.1.4
- 4 Measurements (skimage): 0.1.4
- StarDist: 2021.6.28
- aicsimageio-in-memory: 0.3.5
- aicsimageio-out-of-memory: 0.3.5
- animation: 0.0.2
- clEsperanto: 0.9.10
- console: 0.0.4
- ome-types: 0.2.6
- scikit-image: 0.4.11
- svg: 0.1.5
- video

```




Discussion:
@psobolewskiPhD I do not understand the issue, could you elaborate, or give a reproducible example?
I'm not sure how? There's simply a 1-px border—I think it's the canvas—around the image. In light mode, it's white, which is different from the rest of the viewer, so it's more easily noticed. Even then it's subtle, if you have a black image background, just because of visual perception.

Try:
1. Napari light mode
2. Open Samples>skimage > human_mitosis
3. Set colormap to `gray r`
4. zoom in so the image fills the canvas.
5. Look at the corner
<img width="311" alt="image" src="https://user-images.githubusercontent.com/76622105/132587527-24a88fc3-666b-46f9-9aa7-d33caff71b6d.png">
You should see the 1-px white border around the image.
In dark mode it's black, so it blends with the viewer, but it's there:
<img width="239" alt="image" src="https://user-images.githubusercontent.com/76622105/132587681-c3142636-3bef-46f7-b17a-385e000bee9e.png">
(This is Napari dark mode, Samples>skimage>human_mitosis, colormap=gray) 

Note the black border between the viewer and the grey region in the very corner.
Importantly, this black border can still be seen in screenshots, particularly ones that have a light background:
<img width="235" alt="image" src="https://user-images.githubusercontent.com/76622105/132587903-5b262d6c-b657-45be-8871-c0709b54b23b.png">

This 1-px to me does not appear to be an intentional border around the image, but rather a bug where the viewer/canvas/image areas do not overlap/interact correctly.
Got it, now I understand thanks
I looked a bit into this but I am not sure where that border is coming from 🤔 
I'm pretty sure it's the color of the canvas, so perhaps somewhere the max size of an image layer is 1-px less than the canvas? 
All my images are even number of pixels...maybe it's odd vs even?
🤷 
I think I worked out what's causing this.

First, like @psobolewskiPhD mentioned, this border is the one associated with canvas. Or rather its central widget. You can see that by changing that widget's color:

```python
viewer._window.qt_viewer.canvas.central_widget.border_color = 'green'
```

![Screen Shot 2021-11-12 at 7 14 19 PM](https://user-images.githubusercontent.com/2608297/141604275-88f0a8c6-19c4-4db7-b274-37bb4af0bd96.png)

`VispyCanvas.central_widget` is a vispy `Widget`, so I took a look at that class and saw `Widget._border_width` which had a value of 1.

That value is set on initialization using the `border_width` parameter (which defaults to 1). Therefore, I thought something like the following would remove it:

```python
viewer._window.qt_viewer.canvas.central_widget._border_width = 0
viewer._window.qt_viewer.canvas.central_widget.update_line()
```

which removed the green border, but a black border was still there.

![Screen Shot 2021-11-12 at 7 15 08 PM](https://user-images.githubusercontent.com/2608297/141604282-5ee5512e-e43d-4288-a44f-f3c51ffab595.png)

Then I realized that there could other widgets in the hierarchy with the same default transparent 1-pixel border. So I temporarily modified [the source of vispy's `Widget`](https://github.com/vispy/vispy/blob/d0d40f9ba4033e89f4110fa8f26ed126f7d1b243/vispy/scene/widgets/widget.py#L45) to make the default value of `border_width` 0.

And that seems to work.

![Screen Shot 2021-11-12 at 7 20 58 PM](https://user-images.githubusercontent.com/2608297/141604287-675024cd-523f-4cb7-8525-701f5c406a04.png)

However, I'm not sure what the best fix is for this - here are some ideas.

1. Live with the border in the viewer, but remove it in the screenshot.
2. Find all the vispy `Widgets` and update all their `_border_width` values to be 0. I haven't confirmed this works yet.
3. Add a modified version of `Widget` to our vendored vispy code.
4. Open a PR to make the default `border_width` 0, or at least make the default configurable for a canvas.

Any other ideas are welcome!
Wow, awesome detective work.
From my point of view:
In live napari the border—once you see it—is merely a nuisance, but in screenshots it's really annoying to have to crop out for clean figures. So option 1 would be perfectly fine.

Long term, the idea of a persistent 1 px border for all widgets by default seems, odd? Almost like test code leftover? By analogy, when I'm making tables I have cell borders enabled to see how everything is working out. But then I turn them off so the table doesn't look like a brick wall. So I think option 4 would be worth it!
Thanks for the comments @psobolewskiPhD. I reached out to the vispy maintainers on [their gitter](https://gitter.im/vispy/vispy) to check whether the border is intended and if they can think of any other possible fixes. Or if they would be open to a PR.

Resolving patch:
diff --git a/napari/_qt/qt_viewer.py b/napari/_qt/qt_viewer.py
--- a/napari/_qt/qt_viewer.py
+++ b/napari/_qt/qt_viewer.py
@@ -347,7 +347,7 @@ def __init__(self, viewer: Viewer, show_welcome_screen: bool = False):
         for layer in self.viewer.layers:
             self._add_layer(layer)
 
-        self.view = self.canvas.central_widget.add_view()
+        self.view = self.canvas.central_widget.add_view(border_width=0)
         self.camera = VispyCamera(
             self.view, self.viewer.camera, self.viewer.dims
         )
diff --git a/napari/_vispy/canvas.py b/napari/_vispy/canvas.py
--- a/napari/_vispy/canvas.py
+++ b/napari/_vispy/canvas.py
@@ -1,7 +1,7 @@
 """VispyCanvas class.
 """
 from qtpy.QtCore import QSize
-from vispy.scene import SceneCanvas
+from vispy.scene import SceneCanvas, Widget
 
 from ..utils.colormaps.standardize_color import transform_color
 from .utils.gl import get_max_texture_sizes
@@ -77,6 +77,15 @@ def bgcolor(self, value):
         _value = self._background_color_override or value
         SceneCanvas.bgcolor.fset(self, _value)
 
+    @property
+    def central_widget(self):
+        """Overrides SceneCanvas.central_widget to make border_width=0"""
+        if self._central_widget is None:
+            self._central_widget = Widget(
+                size=self.size, parent=self.scene, border_width=0
+            )
+        return self._central_widget
+
     def _process_mouse_event(self, event):
         """Ignore mouse wheel events which have modifiers."""
         if event.type == 'mouse_wheel' and len(event.modifiers) > 0:


Test patch:
diff --git a/napari/_tests/test_with_screenshot.py b/napari/_tests/test_with_screenshot.py
--- a/napari/_tests/test_with_screenshot.py
+++ b/napari/_tests/test_with_screenshot.py
@@ -503,3 +503,20 @@ def test_scale_bar_visible(make_napari_viewer):
     off_screenshot = viewer.screenshot(canvas_only=True, flash=False)
     assert not viewer.scale_bar.visible
     np.testing.assert_almost_equal(launch_screenshot, off_screenshot)
+
+
+@slow(30)
+@skip_on_win_ci
+@skip_local_popups
+def test_screenshot_has_no_border(make_napari_viewer):
+    """See https://github.com/napari/napari/issues/3357"""
+    viewer = make_napari_viewer(show=True)
+    image_data = np.ones((60, 80))
+    viewer.add_image(image_data, colormap='red')
+    # Zoom in dramatically to make the screenshot all red.
+    viewer.camera.zoom = 1000
+
+    screenshot = viewer.screenshot(canvas_only=True, flash=False)
+
+    expected = np.broadcast_to([255, 0, 0, 255], screenshot.shape)
+    np.testing.assert_array_equal(screenshot, expected)