Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Screenshot without margins #7

Open
wants to merge 86 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
86 commits
Select commit Hold shift + click to select a range
6873d6c
remove 2d black margins screenshot
melonora Mar 8, 2024
5948051
add param to viewer.screenshot
melonora Mar 8, 2024
d5c0c72
Merge branch 'main' into screenshot_without_margins
melonora May 5, 2024
4cc0d90
fix 1 pixel off
melonora May 5, 2024
6364fd0
Co-Authored-By: olusesan.ajina@gmail.com
melonora May 5, 2024
fed5a73
Merge branch 'screenshot_without_margins' of https://github.com/melon…
melonora May 5, 2024
d3831bd
add test
melonora May 5, 2024
d012489
fix tests
melonora May 5, 2024
4d9b73f
chance parameter name
melonora May 5, 2024
9212a9f
fix tests
melonora May 5, 2024
d091e32
rename to no_margins
melonora May 5, 2024
9daa6cb
switch to margins parameter and default old behaviour
melonora May 6, 2024
feae403
Merge branch 'main' into screenshot_without_margins
melonora May 8, 2024
221692c
revert to fit_to_data
melonora May 12, 2024
41a8326
disallow fit_to_data if canvas_only is False
melonora May 18, 2024
10e86de
adjust test
melonora May 18, 2024
bffcbb2
fix test camera center
melonora May 24, 2024
e0e2498
close viewer prevent dangling animation
melonora May 26, 2024
effce1e
add fit_to_data to nbscreenshot
melonora May 26, 2024
3a79f08
Update napari/components/viewer_model.py
melonora May 27, 2024
0997e89
change parameter
melonora May 27, 2024
33a2a32
update scale_factor calc
melonora May 27, 2024
4bb7854
Update napari/_qt/qt_main_window.py
melonora May 27, 2024
e2ebf1b
update error messages
melonora May 27, 2024
31e8e9a
Merge branch 'screenshot_without_margins' of https://github.com/melon…
melonora May 27, 2024
2f71728
update docstrings
melonora May 27, 2024
6fcf9be
fix error
melonora May 27, 2024
7635530
minor grammar fix + fit docstring in 80c
jni May 27, 2024
65729bb
Make error strings fit in 80c
jni May 27, 2024
1d3140c
Update docstring for screenshot
jni May 27, 2024
5d50d64
Fix outdated docstring for Viewer.reset_view
jni May 27, 2024
8e5cba5
Fix fit-to-data docstring in two more places
jni May 27, 2024
7eabe99
Fix test error message match
jni May 27, 2024
55d6db9
rename parameter
melonora May 27, 2024
7105658
change to export_view
melonora Jun 1, 2024
6afe24b
address comments
melonora Jun 14, 2024
f622097
remove docstring
melonora Jun 14, 2024
df0b991
Merge branch 'main' into screenshot_without_margins
melonora Jun 14, 2024
cb1e951
fix test
melonora Jun 14, 2024
6984781
Merge branch 'main' into screenshot_without_margins
melonora Jun 14, 2024
21feada
revert to FutureWarning
melonora Jun 14, 2024
b840574
Merge branch 'screenshot_without_margins' of https://github.com/melon…
melonora Jun 14, 2024
9fa60ca
change to export_figure
melonora Jun 20, 2024
933cb7f
Merge branch 'main' into screenshot_without_margins
melonora Jun 22, 2024
4b6c429
Match docstring formatting to PEP257 and clarify scale
jni Jun 22, 2024
21f03cb
Update napari/_qt/_tests/test_qt_viewer.py
melonora Jun 23, 2024
a2eee46
adjust docstring
melonora Jun 23, 2024
90ca6d3
typehints
melonora Jun 23, 2024
657eaac
typehints
melonora Jun 23, 2024
5cb8a55
typehints
melonora Jun 23, 2024
a80e64f
typehints
melonora Jun 23, 2024
b0eed32
Update napari/components/viewer_model.py
melonora Jun 23, 2024
c142c3c
add example
melonora Jun 23, 2024
6e2a16c
Merge branch 'screenshot_without_margins' of https://github.com/melon…
melonora Jun 23, 2024
5cccde9
set default scale to 1, adjust docstring
melonora Jun 23, 2024
fbe7c58
adjust example and docstrings
melonora Jun 23, 2024
74aa262
adjust explanation docstring
melonora Jun 23, 2024
e28e9d3
Only allow float or int
melonora Jun 23, 2024
b511309
fix isinstance
melonora Jun 23, 2024
32f9bb3
Merge branch 'main' into screenshot_without_margins
jni Jun 26, 2024
3b1fad7
Update napari/_qt/qt_main_window.py
melonora Jul 1, 2024
343182a
Apply suggestions from code review
melonora Jul 1, 2024
04a2ba2
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jul 1, 2024
667795f
add missed import
Czaki Jul 1, 2024
9137b27
Update napari/_qt/_tests/test_qt_viewer.py
Czaki Jul 1, 2024
25167dc
Update napari/viewer.py
melonora Jul 2, 2024
57a93df
use extent.step
melonora Jul 2, 2024
8cb09bf
Merge branch 'screenshot_without_margins' of https://github.com/melon…
melonora Jul 2, 2024
f6ff4bf
remove conditional
melonora Jul 2, 2024
dde2642
adjust docstring
melonora Jul 2, 2024
25596a8
adjust docstring
melonora Jul 2, 2024
9b1a230
scale -> scale_factor in gallery example
jni Jul 5, 2024
eb89264
Add some missing layer actions tests (split rgb, split and merge acti…
dalthviz Jul 6, 2024
7646145
Restore events to `QtViewer.canvas` (#7060)
Czaki Jul 6, 2024
4296ac5
Use minimum step size across all dims
jni Jul 6, 2024
e8c3f6f
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jul 6, 2024
f5fe4f4
Remove f-string from translation call
jni Jul 6, 2024
374a6df
Move `IO_Utilities` and `Acquire` submenus to their own group in the …
DragaDoncila Jul 6, 2024
46fdf4c
Add empty menu placeholder actions using functional context keys from…
DragaDoncila Jul 6, 2024
bc1d95f
Add warning in docstring about ignored size
jni Jul 7, 2024
2bd587d
Minor docstring clarification
jni Jul 7, 2024
4d06794
Fix incorrect setting of scale in test
jni Jul 7, 2024
29f6d66
Use allclose to test screenshot size when rounding
jni Jul 7, 2024
de4bb80
Refactor screenshot function to clarify logic flow
jni Jul 7, 2024
77bf7ed
Docstring and typing fixes
jni Jul 7, 2024
2935b04
Merge branch 'main' into screenshot_without_margins
jni Jul 7, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 110 additions & 0 deletions examples/export_figure.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
"""
Export Figure
=============

Display one shapes layer ontop of one image layer using the ``add_shapes`` and
``add_image`` APIs. When the window is closed it will print the coordinates of
your shapes.

.. tags:: visualization-advanced
"""

import numpy as np
from skimage import data

import napari

# create the viewer and window
viewer = napari.Viewer()

# add the image
img_layer = viewer.add_image(data.camera(), name='photographer')
img_layer.colormap = 'gray'

# create a list of polygons
polygons = [
np.array([[11, 13], [111, 113], [22, 246]]),
np.array(
[
[505, 60],
[402, 71],
[383, 42],
[251, 95],
[212, 59],
[131, 137],
[126, 187],
[191, 204],
[171, 248],
[211, 260],
[273, 243],
[264, 225],
[430, 173],
[512, 160],
]
),
np.array(
[
[310, 382],
[229, 381],
[209, 401],
[221, 411],
[258, 411],
[300, 412],
[306, 435],
[268, 434],
[265, 454],
[298, 461],
[307, 461],
[307, 507],
[349, 510],
[352, 369],
[330, 366],
[330, 366],
]
),
]

# add polygons
layer = viewer.add_shapes(
polygons,
shape_type='polygon',
edge_width=1,
edge_color='coral',
face_color='royalblue',
name='shapes',
)

# add an ellipse to the layer
ellipse = np.array([[59, 222], [110, 289], [170, 243], [119, 176]])
layer.add(
ellipse,
shape_type='ellipse',
edge_width=5,
edge_color='coral',
face_color='purple',
)

labels = layer.to_labels([512, 512])
labels_layer = viewer.add_labels(labels, name='labels')

points = np.array([[100, 100], [200, 200], [333, 111]])
size = np.array([10, 20, 20])
viewer.add_points(points, size=size)

# Export figure and change theme before and after exporting to show that the background canvas margins
# are not in the exported figure.
viewer.theme = "light"
# Optionally for saving the exported figure: viewer.export_figure(path="export_figure.png")
export_figure = viewer.export_figure()
scaled_export_figure = viewer.export_figure(scale_factor=5)
viewer.theme = "dark"

viewer.add_image(export_figure, rgb=True, name='exported_figure')
viewer.add_image(scaled_export_figure, rgb=True, name='scaled_exported_figure')
viewer.reset_view()

# from skimage.io import imsave
# imsave('screenshot.png', screenshot)

if __name__ == '__main__':
napari.run()
7 changes: 4 additions & 3 deletions napari/_app_model/constants/_menus.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,9 +96,10 @@ class MenuGroup:
PLUGIN_SINGLE_CONTRIBUTIONS = '3_plugin_contributions'
# File menubar
OPEN = '1_open'
PREFERENCES = '2_preferences'
SAVE = '3_save'
CLOSE = '4_close'
UTIL = '2_util'
PREFERENCES = '3_preferences'
SAVE = '4_save'
CLOSE = '5_close'

class LAYERLIST_CONTEXT:
CONVERSION = '1_conversion'
Expand Down
95 changes: 95 additions & 0 deletions napari/_app_model/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
from typing import Union

from app_model.expressions import parse_expression
from app_model.types import Action, MenuItem, SubmenuItem

from napari._app_model import get_app
from napari._app_model.constants import MenuGroup, MenuId

MenuOrSubmenu = Union[MenuItem, SubmenuItem]


def contains_dummy_action(menu_items: list[MenuOrSubmenu]) -> bool:
"""True if one of the menu_items is the dummy action, otherwise False.

Parameters
----------
menu_items : list[MenuOrSubmenu]
menu items belonging to a given menu

Returns
-------
bool
True if menu_items contains dummy item otherwise false
"""
for item in menu_items:
if hasattr(item, 'command') and 'empty_dummy' in item.command.id:
return True
return False


def is_empty_menu(menu_id: str) -> bool:
"""Return True if the given menu_id is empty, otherwise False

Parameters
----------
menu_id : str
id of the menu to check

Returns
-------
bool
True if the given menu_id is empty, otherwise False
"""
app = get_app()
if menu_id not in app.menus:
return True
if len(app.menus.get_menu(menu_id)) == 0:
return True
if len(app.menus.get_menu(menu_id)) == 1 and contains_dummy_action(
app.menus.get_menu(menu_id)
):
return True
return False


def no_op() -> None:
"""Fully qualified no-op to use for dummy actions."""


def get_dummy_action(menu_id: MenuId) -> tuple[Action, str]:
"""Return a dummy action to be used for the given menu.

The part of the menu_id after the final `/` will form
a unique id_key used for the action ID and the when
expression context key.

Parameters
----------
menu_id: MenuId
id of the menu to add the dummy action to

Returns
-------
tuple[Action, str]
dummy action and the `when` expression context key
"""
# NOTE: this assumes the final word of each contributable
# menu path is unique, otherwise, we will clash. Once we
# move to using short menu keys, the key itself will be used
# here and this will no longer be a concern.
id_key = menu_id.split('/')[-1]
action = Action(
id=f'napari.{id_key}.empty_dummy',
title='Empty',
callback=no_op,
menus=[
{
'id': menu_id,
'group': MenuGroup.NAVIGATION,
'when': parse_expression(context_key := f'{id_key}_empty'),
}
],
enablement=False,
)
return action, context_key
36 changes: 35 additions & 1 deletion napari/_qt/_qapp_model/qactions/__init__.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
from __future__ import annotations

from functools import lru_cache
from functools import lru_cache, partial
from itertools import chain
from typing import TYPE_CHECKING

from napari._qt._qapp_model.injection._qprocessors import QPROCESSORS
from napari._qt._qapp_model.injection._qproviders import QPROVIDERS

if TYPE_CHECKING:
from app_model.types import Context

# Submodules should be able to import from most modules, so to
# avoid circular imports, don't import submodules at the top level here,
# import them inside the init_qactions function.
Expand Down Expand Up @@ -84,3 +88,33 @@ def init_qactions() -> None:
app.menus.append_menu_items(
chain(FILE_SUBMENUS, VIEW_SUBMENUS, DEBUG_SUBMENUS, LAYERS_SUBMENUS)
)


def add_dummy_actions(context: Context) -> None:
"""Register dummy 'Empty' actions for all contributable menus.

Each action is registered with its own `when` condition, that
ensures the action is not visible once the menu is populated.
The context key used in the `when` condition is also added to
the given `context` and assigned to a partial function that
returns True if the menu is empty, and otherwise False.


Parameters
----------
context : Context
context to store functional keys used in `when` conditions
"""
from napari._app_model import get_app
from napari._app_model.constants._menus import MenuId
from napari._app_model.utils import get_dummy_action, is_empty_menu

app = get_app()

actions = []
for menu_id in MenuId.contributables():
dummmy_action, context_key = get_dummy_action(menu_id)
if dummmy_action.id not in app.commands:
actions.append(dummmy_action)
context[context_key] = partial(is_empty_menu, menu_id)
app.register_actions(actions)
4 changes: 2 additions & 2 deletions napari/_qt/_qapp_model/qactions/_file.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@
SubmenuItem(
submenu=MenuId.FILE_IO_UTILITIES,
title=trans._('IO Utilities'),
group=MenuGroup.OPEN,
group=MenuGroup.UTIL,
order=101,
),
),
Expand All @@ -64,7 +64,7 @@
SubmenuItem(
submenu=MenuId.FILE_ACQUIRE,
title=trans._('Acquire'),
group=MenuGroup.OPEN,
group=MenuGroup.UTIL,
order=101,
),
),
Expand Down
43 changes: 42 additions & 1 deletion napari/_qt/_tests/test_qt_viewer.py
Original file line number Diff line number Diff line change
Expand Up @@ -264,11 +264,52 @@ def test_screenshot(make_napari_viewer):

# Take screenshot
with pytest.warns(FutureWarning):
screenshot = viewer.window.qt_viewer.screenshot(flash=False)
viewer.window.qt_viewer.screenshot(flash=False)
screenshot = viewer.window.screenshot(flash=False, canvas_only=True)
assert screenshot.ndim == 3


def test_export_figure(make_napari_viewer, tmp_path):
viewer = make_napari_viewer()

np.random.seed(0)
# Add image
data = np.random.randint(150, 250, size=(250, 250))
layer = viewer.add_image(data)

camera_center = viewer.camera.center
camera_zoom = viewer.camera.zoom
img = viewer.export_figure(flash=False, path=str(tmp_path / 'img.png'))

assert viewer.camera.center == camera_center
assert viewer.camera.zoom == camera_zoom
assert img.shape == (250, 250, 4)
assert np.all(img != np.array([0, 0, 0, 0]))

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This only fails when running the test itself. If you use the same code in a python script it does not fail. For some reason, there is no response to the canvas resize event when running inside a test.

assert (tmp_path / 'img.png').exists()

layer.scale = [0.12, 0.24]
img = viewer.export_figure(flash=False)
# allclose accounts for rounding errors when computing size in hidpi aka
# retina displays
np.testing.assert_allclose(img.shape, (250, 499, 4), atol=1)

layer.scale = [0.12, 0.12]
img = viewer.export_figure(flash=False)
assert img.shape == (250, 250, 4)

viewer.camera.center = [100, 100]
camera_center = viewer.camera.center
camera_zoom = viewer.camera.zoom
img = viewer.export_figure()

assert viewer.camera.center == camera_center
assert viewer.camera.zoom == camera_zoom
assert img.shape == (250, 250, 4)
assert np.all(img != np.array([0, 0, 0, 0]))
viewer.close()


@pytest.mark.skip('new approach')
def test_screenshot_dialog(make_napari_viewer, tmpdir):
"""Test save screenshot functionality."""
Expand Down
Loading
Loading