jupyter_compare_view 0.2.5__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2022 Octoframes
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,204 @@
1
+ Metadata-Version: 2.4
2
+ Name: jupyter_compare_view
3
+ Version: 0.2.5
4
+ Summary: Blend Between Multiple Images in JupyterLab.
5
+ License: MIT
6
+ License-File: LICENSE
7
+ Author: Octoframes
8
+ Requires-Python: >=3.8,<3.13
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.8
12
+ Classifier: Programming Language :: Python :: 3.9
13
+ Classifier: Programming Language :: Python :: 3.10
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Requires-Dist: Jinja2 (>=2.11.3)
17
+ Requires-Dist: Pillow (>=7.1.2)
18
+ Requires-Dist: ipykernel (>=5.0.0)
19
+ Requires-Dist: ipython (>=6.0.0)
20
+ Description-Content-Type: text/markdown
21
+
22
+ # Jupyter compare_view
23
+
24
+ ![bannerFINAL](https://user-images.githubusercontent.com/44469195/179508322-ea10e22a-6dfb-47f4-8fbb-d5ce724f0127.png)
25
+
26
+ [![JupyterLight](https://jupyterlite.rtfd.io/en/latest/_static/badge.svg)](https://octoframes.github.io/jupyter_compare_view)
27
+ [![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/Octoframes/jupyter_compare_view/HEAD?labpath=example_notebook.ipynb)
28
+ [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/Octoframes/jupyter_compare_view/blob/main/example_notebook.ipynb)
29
+ [![PyPI version](https://badge.fury.io/py/jupyter_compare_view.svg)](https://badge.fury.io/py/jupyter_compare_view)
30
+ [![MIT](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/Octoframes/jupyter_compare_view/blob/main/LICENSE)
31
+
32
+
33
+ Blend between multiple images using a cell magic in JupyterLab using [compare_view](https://octoframes.github.io/compare_view).
34
+ *This project was called jupyter-splitview before.*
35
+
36
+
37
+ ## Installation
38
+ ```py
39
+ pip install jupyter_compare_view
40
+ ```
41
+ ## Example
42
+ ```py
43
+ import jupyter_compare_view
44
+ ```
45
+
46
+ ```py
47
+ %%compare
48
+ from skimage import data
49
+ from skimage.color import rgb2gray
50
+ import matplotlib.pyplot as plt
51
+
52
+ img = data.chelsea()
53
+ grayscale_img = rgb2gray(img)
54
+
55
+ plt.imshow(img)
56
+ plt.axis("off")
57
+ plt.show()
58
+
59
+ plt.imshow(grayscale_img, cmap="gray")
60
+ plt.axis("off")
61
+ plt.show()
62
+ ```
63
+
64
+ <img src="https://user-images.githubusercontent.com/44469195/179499138-65160434-11e1-4358-8e25-5b26ba9ebf4a.png" style="width: 400px;"/>
65
+
66
+ Another example:
67
+ ```py
68
+ from jupyter_compare_view import compare
69
+
70
+ compare(img, grayscale_img, cmap="gray", start_mode="horizontal", start_slider_pos=0.73)
71
+ ```
72
+ <img src="https://user-images.githubusercontent.com/44469195/179499350-94244408-cabf-4945-affc-fd0444d53555.png" style="width: 400px;"/>
73
+
74
+
75
+ The split view widget is still responsive after closing and reopening the notebook without running the cell again.
76
+
77
+ ## Notebook arguments
78
+ (Might still change in future)
79
+ * `--config '{"start_mode": "horizontal"}'` will init the compare-view in horizontal slider mode.
80
+ * `--config '{"circle_size": 30}'` the circle size is now 30 pixel in circle mode.
81
+ * `--config '{"show_slider": false}'` will hide the slider bar.
82
+ * `--config '{"start_slider_pos": 0.73}'` will set the slider start position to 73%.
83
+
84
+ * *Removed in 0.1.1: `--position 73%` will no longer the slider start position to 73%.*
85
+ * `--config '{"start_mode": "horizontal","start_slider_pos": 0.73}'` will both set the start mode to horizontal and set the slider position
86
+ * `--height 220` will set the height to 220 pixel.
87
+ * When `--height`is not provided, the default height of the widget is 300 pixel.
88
+ * `--height auto` will set the height by the value of the first image's resolution in vertical direction.
89
+ * The widget's width will always be adjusted automatically.
90
+
91
+ ## Notebook formatting
92
+ Formatting with black can be done this way:
93
+ 1. `pip install 'black[jupyter]'`
94
+ 2. `black --python-cell-magics compare compare_view_magic.ipynb`
95
+
96
+ ## Interactive Export
97
+ jupyter_compare_view fully supports offline interactive HTML exports.
98
+ The web library [compare_view](https://octoframes.github.io/compare_view) is inlined into the exported HTML document.
99
+ Therefore, the export is viewable without an internet connection.
100
+
101
+ ![Simply export your notebook as HTML to allow interacting with your data without jupyter.](html_export_screenshot.png)
102
+
103
+
104
+ ## Developer Installation
105
+
106
+ 1. `git clone --recurse https://github.com/Octoframes/jupyter_compare_view`
107
+ (Note: In case that the repo was already cloned e.g. with the GitHub Desktop client, the GitHub submodule has to be loaded via `git submodule update --init --recursive`)
108
+ 2. `poetry install`
109
+
110
+ *Note*: The IPython extension `autoreload` reloads modules before every cell execution. Very useful when debugging the `%%capture` cell magic!
111
+ Just add these lines into the first jupyter cell.
112
+ ```py
113
+ %load_ext autoreload
114
+ %autoreload 2
115
+ import jupyter_compare_view
116
+ ```
117
+
118
+ ## Changelog
119
+
120
+ # 0.2.4
121
+
122
+ CHange to importlib [#48](https://github.com/Octoframes/jupyter_compare_view/pull/48)
123
+
124
+ ## 0.2.3
125
+
126
+ Remove setuptools dependency
127
+ ## 0.2.2
128
+
129
+ * Remove python3.7 support [#46](https://github.com/Octoframes/jupyter_compare_view/pull/46)
130
+ * fix jupyterlite example
131
+ ## 0.2.1
132
+
133
+ * Support python 3.11
134
+
135
+ ## 0.2.0
136
+
137
+ * Implemented `capture` to display the compare view frame without calling the cell magic. This is not an ipywidget as mentioned [here](https://github.com/Octoframes/jupyter_compare_view/pull/41#pullrequestreview-1205327074).
138
+ * Update version requirements [#42](https://github.com/Octoframes/jupyter_compare_view/pull/42/files)
139
+ ## 0.1.5
140
+
141
+ * BugFix: Remove black import that was added by accident.
142
+
143
+ ## 0.1.4
144
+
145
+ * `%%compare` is now `%%splity`. `%%splity` is deprecated.
146
+ * Update examples
147
+
148
+ ## 0.1.3
149
+
150
+ * octoframes github actions setup
151
+
152
+ ## 0.1.2
153
+
154
+ * Move the repo from kolibril13/jupyter-spitview to octoframes/jupyter_compare_view
155
+ * Rename all references
156
+ ## 0.1.1
157
+
158
+ * Drop the [github.com/NUKnightLab/juxtapose](https://github.com/NUKnightLab/juxtapose) backend and replace it with [github.com/Octoframes/compare_view](https://github.com/Octoframes/compare_view).
159
+ * Implement horizontal slider
160
+ * Implement Round Mask
161
+ ## 0.1.0
162
+
163
+ * Update dependencies
164
+ * Update JupyterLite version
165
+ * Fix: in JupyterLite, a figure has to be explicitly called by plt.show()
166
+ * Better installation workflow
167
+
168
+ ## 0.0.8
169
+
170
+ * Fixing problem with cell id and notebook reloading
171
+ * Experimentally lowering the dependencies to
172
+ `ipython = ">=6.0.0"` and `ipykernel = ">=5.0.0"` so that jupyterlite will work hopefully.
173
+
174
+ ## 0.0.7
175
+
176
+ * Rewrite of the import of JavaScript and CSS to make it more robust when closing and opening the notebook
177
+ * First attempt to add a JupyterLite example.
178
+ ## 0.0.6
179
+
180
+ Fix poetry workflow
181
+
182
+ ## 0.0.5
183
+
184
+ * Ship the javascript directly with the package, so no internet connection is required
185
+ * use jinja2 to save HTML in separate file
186
+ * load stylesheet and javascript only once in the beginning, and not in every cell that contains the splitview widget.
187
+
188
+ ## 0.0.4
189
+
190
+ * New `--height` parameter
191
+
192
+ ## 0.0.3
193
+
194
+ * default slider position
195
+ * updated minimal example
196
+ * internal code restructuring and formatting
197
+ * Handle import in non jupyter context
198
+
199
+ ## 0.0.2
200
+ * save images in base64 strings and don't load images to disk (increases package security).
201
+ ## 0.0.1
202
+
203
+ * First release
204
+
@@ -0,0 +1,182 @@
1
+ # Jupyter compare_view
2
+
3
+ ![bannerFINAL](https://user-images.githubusercontent.com/44469195/179508322-ea10e22a-6dfb-47f4-8fbb-d5ce724f0127.png)
4
+
5
+ [![JupyterLight](https://jupyterlite.rtfd.io/en/latest/_static/badge.svg)](https://octoframes.github.io/jupyter_compare_view)
6
+ [![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/Octoframes/jupyter_compare_view/HEAD?labpath=example_notebook.ipynb)
7
+ [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/Octoframes/jupyter_compare_view/blob/main/example_notebook.ipynb)
8
+ [![PyPI version](https://badge.fury.io/py/jupyter_compare_view.svg)](https://badge.fury.io/py/jupyter_compare_view)
9
+ [![MIT](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/Octoframes/jupyter_compare_view/blob/main/LICENSE)
10
+
11
+
12
+ Blend between multiple images using a cell magic in JupyterLab using [compare_view](https://octoframes.github.io/compare_view).
13
+ *This project was called jupyter-splitview before.*
14
+
15
+
16
+ ## Installation
17
+ ```py
18
+ pip install jupyter_compare_view
19
+ ```
20
+ ## Example
21
+ ```py
22
+ import jupyter_compare_view
23
+ ```
24
+
25
+ ```py
26
+ %%compare
27
+ from skimage import data
28
+ from skimage.color import rgb2gray
29
+ import matplotlib.pyplot as plt
30
+
31
+ img = data.chelsea()
32
+ grayscale_img = rgb2gray(img)
33
+
34
+ plt.imshow(img)
35
+ plt.axis("off")
36
+ plt.show()
37
+
38
+ plt.imshow(grayscale_img, cmap="gray")
39
+ plt.axis("off")
40
+ plt.show()
41
+ ```
42
+
43
+ <img src="https://user-images.githubusercontent.com/44469195/179499138-65160434-11e1-4358-8e25-5b26ba9ebf4a.png" style="width: 400px;"/>
44
+
45
+ Another example:
46
+ ```py
47
+ from jupyter_compare_view import compare
48
+
49
+ compare(img, grayscale_img, cmap="gray", start_mode="horizontal", start_slider_pos=0.73)
50
+ ```
51
+ <img src="https://user-images.githubusercontent.com/44469195/179499350-94244408-cabf-4945-affc-fd0444d53555.png" style="width: 400px;"/>
52
+
53
+
54
+ The split view widget is still responsive after closing and reopening the notebook without running the cell again.
55
+
56
+ ## Notebook arguments
57
+ (Might still change in future)
58
+ * `--config '{"start_mode": "horizontal"}'` will init the compare-view in horizontal slider mode.
59
+ * `--config '{"circle_size": 30}'` the circle size is now 30 pixel in circle mode.
60
+ * `--config '{"show_slider": false}'` will hide the slider bar.
61
+ * `--config '{"start_slider_pos": 0.73}'` will set the slider start position to 73%.
62
+
63
+ * *Removed in 0.1.1: `--position 73%` will no longer the slider start position to 73%.*
64
+ * `--config '{"start_mode": "horizontal","start_slider_pos": 0.73}'` will both set the start mode to horizontal and set the slider position
65
+ * `--height 220` will set the height to 220 pixel.
66
+ * When `--height`is not provided, the default height of the widget is 300 pixel.
67
+ * `--height auto` will set the height by the value of the first image's resolution in vertical direction.
68
+ * The widget's width will always be adjusted automatically.
69
+
70
+ ## Notebook formatting
71
+ Formatting with black can be done this way:
72
+ 1. `pip install 'black[jupyter]'`
73
+ 2. `black --python-cell-magics compare compare_view_magic.ipynb`
74
+
75
+ ## Interactive Export
76
+ jupyter_compare_view fully supports offline interactive HTML exports.
77
+ The web library [compare_view](https://octoframes.github.io/compare_view) is inlined into the exported HTML document.
78
+ Therefore, the export is viewable without an internet connection.
79
+
80
+ ![Simply export your notebook as HTML to allow interacting with your data without jupyter.](html_export_screenshot.png)
81
+
82
+
83
+ ## Developer Installation
84
+
85
+ 1. `git clone --recurse https://github.com/Octoframes/jupyter_compare_view`
86
+ (Note: In case that the repo was already cloned e.g. with the GitHub Desktop client, the GitHub submodule has to be loaded via `git submodule update --init --recursive`)
87
+ 2. `poetry install`
88
+
89
+ *Note*: The IPython extension `autoreload` reloads modules before every cell execution. Very useful when debugging the `%%capture` cell magic!
90
+ Just add these lines into the first jupyter cell.
91
+ ```py
92
+ %load_ext autoreload
93
+ %autoreload 2
94
+ import jupyter_compare_view
95
+ ```
96
+
97
+ ## Changelog
98
+
99
+ # 0.2.4
100
+
101
+ CHange to importlib [#48](https://github.com/Octoframes/jupyter_compare_view/pull/48)
102
+
103
+ ## 0.2.3
104
+
105
+ Remove setuptools dependency
106
+ ## 0.2.2
107
+
108
+ * Remove python3.7 support [#46](https://github.com/Octoframes/jupyter_compare_view/pull/46)
109
+ * fix jupyterlite example
110
+ ## 0.2.1
111
+
112
+ * Support python 3.11
113
+
114
+ ## 0.2.0
115
+
116
+ * Implemented `capture` to display the compare view frame without calling the cell magic. This is not an ipywidget as mentioned [here](https://github.com/Octoframes/jupyter_compare_view/pull/41#pullrequestreview-1205327074).
117
+ * Update version requirements [#42](https://github.com/Octoframes/jupyter_compare_view/pull/42/files)
118
+ ## 0.1.5
119
+
120
+ * BugFix: Remove black import that was added by accident.
121
+
122
+ ## 0.1.4
123
+
124
+ * `%%compare` is now `%%splity`. `%%splity` is deprecated.
125
+ * Update examples
126
+
127
+ ## 0.1.3
128
+
129
+ * octoframes github actions setup
130
+
131
+ ## 0.1.2
132
+
133
+ * Move the repo from kolibril13/jupyter-spitview to octoframes/jupyter_compare_view
134
+ * Rename all references
135
+ ## 0.1.1
136
+
137
+ * Drop the [github.com/NUKnightLab/juxtapose](https://github.com/NUKnightLab/juxtapose) backend and replace it with [github.com/Octoframes/compare_view](https://github.com/Octoframes/compare_view).
138
+ * Implement horizontal slider
139
+ * Implement Round Mask
140
+ ## 0.1.0
141
+
142
+ * Update dependencies
143
+ * Update JupyterLite version
144
+ * Fix: in JupyterLite, a figure has to be explicitly called by plt.show()
145
+ * Better installation workflow
146
+
147
+ ## 0.0.8
148
+
149
+ * Fixing problem with cell id and notebook reloading
150
+ * Experimentally lowering the dependencies to
151
+ `ipython = ">=6.0.0"` and `ipykernel = ">=5.0.0"` so that jupyterlite will work hopefully.
152
+
153
+ ## 0.0.7
154
+
155
+ * Rewrite of the import of JavaScript and CSS to make it more robust when closing and opening the notebook
156
+ * First attempt to add a JupyterLite example.
157
+ ## 0.0.6
158
+
159
+ Fix poetry workflow
160
+
161
+ ## 0.0.5
162
+
163
+ * Ship the javascript directly with the package, so no internet connection is required
164
+ * use jinja2 to save HTML in separate file
165
+ * load stylesheet and javascript only once in the beginning, and not in every cell that contains the splitview widget.
166
+
167
+ ## 0.0.4
168
+
169
+ * New `--height` parameter
170
+
171
+ ## 0.0.3
172
+
173
+ * default slider position
174
+ * updated minimal example
175
+ * internal code restructuring and formatting
176
+ * Handle import in non jupyter context
177
+
178
+ ## 0.0.2
179
+ * save images in base64 strings and don't load images to disk (increases package security).
180
+ ## 0.0.1
181
+
182
+ * First release
@@ -0,0 +1,14 @@
1
+ from IPython import get_ipython
2
+ import importlib.metadata
3
+ from .compare import compare, StartMode
4
+ from .sw_cellmagic import CompareViewMagic
5
+
6
+ __version__: str = importlib.metadata.version(__name__)
7
+
8
+
9
+ try:
10
+ ipy = get_ipython()
11
+ ipy.register_magics(CompareViewMagic)
12
+ print(f"Jupyter compare_view v{__version__}")
13
+ except AttributeError:
14
+ print("Can not load CompareViewMagic because this is not a notebook")
@@ -0,0 +1,131 @@
1
+ import base64
2
+ import enum
3
+ import io
4
+ import json
5
+ import typing
6
+ import uuid
7
+ from pathlib import Path
8
+ from jinja2 import Template, StrictUndefined
9
+ import IPython
10
+ import PIL
11
+
12
+
13
+ ImageLike = typing.TypeVar('ImageLike')
14
+ ImageSource = typing.Union[str, bytes, ImageLike]
15
+
16
+
17
+ def img2bytes(img: ImageLike, format: str, cmap: str) -> bytes:
18
+ with io.BytesIO() as im_file:
19
+ if isinstance(img, PIL.Image.Image):
20
+ img.save(im_file, format=format)
21
+ else:
22
+ # anything other that can be displayed with plt.imshow
23
+ import matplotlib.pyplot as plt
24
+
25
+ plt.imsave(im_file, img, format=format, cmap=cmap)
26
+ return im_file.getvalue()
27
+
28
+
29
+ def img2url(img: ImageSource, format: str, cmap: str) -> str:
30
+ if isinstance(img, str):
31
+ return img.strip()
32
+ if isinstance(img, bytes):
33
+ data = img
34
+ else:
35
+ data = img2bytes(img, format=format, cmap=cmap)
36
+ return f"data:image/{format};base64,{str(base64.b64encode(data), 'utf8')}"
37
+
38
+
39
+ def compile_template(in_file: str, **variables) -> str:
40
+ with open(in_file, "r", encoding="utf-8") as file:
41
+ template = Template(file.read(), undefined=StrictUndefined)
42
+ return template.render(**variables)
43
+
44
+
45
+ def prepare_html(image_urls: typing.List[str], height: str, add_controls: bool, config: dict) -> str:
46
+ uid=uuid.uuid1()
47
+ config['key'] = str(uid)
48
+ if add_controls:
49
+ config["controls_id"] = f"controls_{uid}"
50
+ root = Path(__file__).parent
51
+ js_path = root / "../vendor/compare_view/browser_compare_view.js"
52
+ js = js_path.read_text()
53
+ return compile_template(
54
+ root / "template.html",
55
+ uid=uid,
56
+ image_urls=image_urls,
57
+ height=height,
58
+ js=js,
59
+ add_controls=add_controls,
60
+ config=json.dumps(config),
61
+ )
62
+
63
+
64
+ @enum.unique
65
+ class StartMode(str, enum.Enum):
66
+ CIRCLE = "circle"
67
+ HORIZONTAL = "horizontal"
68
+ VERTICAL = "vertical"
69
+
70
+
71
+ def compare(
72
+ image1: ImageSource,
73
+ image2: ImageSource,
74
+ *other_images: ImageSource,
75
+ height: typing.Union[str, int] = 'auto',
76
+ add_controls: bool = True,
77
+ start_mode: typing.Union[StartMode, str] = StartMode.CIRCLE,
78
+ circumference_fraction: float = 0.005,
79
+ circle_size: typing.Optional[float] = None,
80
+ circle_fraction: float = 0.2,
81
+ show_circle: bool = True,
82
+ revolve_imgs_on_click: bool = True,
83
+ slider_fraction: float = 0.01,
84
+ slider_time: float = 400,
85
+ # rate_function: str = 'ease_in_out_cubic',
86
+ start_slider_pos: float = 0.5,
87
+ show_slider: bool = True,
88
+ display_format: str = 'jpeg',
89
+ cmap: typing.Optional[str] = None,
90
+ ) -> IPython.display.HTML:
91
+ """
92
+ Args:
93
+ height: height of the widget in pixels or "auto"
94
+ add_controls: pass False to not create controls
95
+ start_mode: either "circle", "horizontal" or "vertical"
96
+ circumference_fraction: size of circle outline as fraction of image width or height (whatever is bigger)
97
+ circle_size: the radius in pixel
98
+ circle_fraction: a fraction of the image width or height (whichever is bigger—called max_size in this document)
99
+ show_circle: draw line around circle
100
+ slider_time: time slider takes to reach clicked location
101
+ start_slider_pos: 0.0 -> left; 1.0 -> right
102
+ show_slider: draw line at slider
103
+ display_format: format used for displaying images
104
+ cmap: colormap for grayscale images
105
+ """
106
+ images = [image1, image2, *other_images]
107
+ image_urls = [
108
+ img2url(img, format=display_format, cmap=cmap) for img in images
109
+ ]
110
+ _locals = locals()
111
+ config = {k: _locals[k] for k in [
112
+ 'start_mode',
113
+ 'circumference_fraction',
114
+ 'circle_fraction',
115
+ 'show_circle',
116
+ 'revolve_imgs_on_click',
117
+ 'slider_fraction',
118
+ 'slider_time',
119
+ # 'rate_function',
120
+ 'start_slider_pos',
121
+ 'show_slider',
122
+ ]
123
+ + ['circle_size'] * (circle_size is not None)
124
+ }
125
+ html = prepare_html(
126
+ image_urls=image_urls,
127
+ height=f'{height}px' if not isinstance(height, str) else height,
128
+ add_controls=add_controls,
129
+ config=config,
130
+ )
131
+ return IPython.display.HTML(html)
@@ -0,0 +1,85 @@
1
+ import io
2
+ import json
3
+
4
+ from IPython.core import magic_arguments
5
+ from IPython.core.magic import Magics, cell_magic, magics_class
6
+ from IPython.utils.capture import capture_output
7
+ from PIL import Image
8
+
9
+ from .compare import compare
10
+
11
+
12
+ @magics_class
13
+ class CompareViewMagic(Magics):
14
+ @magic_arguments.magic_arguments()
15
+ @magic_arguments.argument( # TODO This is currently not used.
16
+ "--position",
17
+ "-p",
18
+ default="50%",
19
+ help="""The start position of the slider. Currently not implemented, use `--config '{"start_slider_pos": 0.73}'` instead""",
20
+ )
21
+ @magic_arguments.argument(
22
+ "--height",
23
+ "-h",
24
+ default="auto",
25
+ help=(
26
+ "The widget's height. The width will be adjusted automatically. \
27
+ If height is `auto`, the vertical resolution of the first image is used."
28
+ ),
29
+ )
30
+ @magic_arguments.argument(
31
+ "--config",
32
+ "-c",
33
+ default="{}",
34
+ help=(
35
+ "The compare view config as described here: https://github.com/Octoframes/compare_view"
36
+ ),
37
+ )
38
+ @cell_magic
39
+ def compare(self, line, cell): # TODO: make a %%splity deprecated version
40
+ """Saves the png image and creates the compare_view canvas"""
41
+
42
+ with capture_output(stdout=False, stderr=False, display=True) as result:
43
+ self.shell.run_cell(cell)
44
+
45
+ # saves all jupyter output images into the out_images_base64 list
46
+ out_images_base64 = []
47
+ for output in result.outputs:
48
+ data = output.data
49
+ if "image/png" in data:
50
+ png_bytes_data = data["image/png"]
51
+ if isinstance(png_bytes_data, str):
52
+ png_bytes_data = f'data:image/png;base64,{png_bytes_data}'
53
+ out_images_base64.append(png_bytes_data)
54
+ if len(out_images_base64) < 2:
55
+ raise ValueError(
56
+ "There need to be at least two images for Jupyter compare_view to work."
57
+ )
58
+
59
+ # get the parameters that configure the widget
60
+ args = magic_arguments.parse_argstring(CompareViewMagic.compare, line)
61
+ height = args.height
62
+
63
+ return compare(
64
+ *out_images_base64,
65
+ **{
66
+ **json.loads(args.config.strip("'").strip('"')),
67
+ "height": height if height == "auto" else int(height)
68
+ }
69
+ )
70
+
71
+ @cell_magic
72
+ def splity(self, line, cell): # TODO: Delete this somewhere 10/2022.
73
+ import warnings
74
+
75
+ print(
76
+ """
77
+ **************************************************************
78
+ Warning: %%splity is deprecated. Please use %%compare instead.
79
+ **************************************************************
80
+ """
81
+ )
82
+ new_line = "%%compare"
83
+ new_line += line
84
+ complete_cell = new_line + "\n" + cell
85
+ self.shell.run_cell(complete_cell)
@@ -0,0 +1,20 @@
1
+ <script>
2
+ {{ js }}
3
+ </script>
4
+
5
+ <div style="display: flex; flex-direction: row; width: 100%;">
6
+ <canvas id="canvas_{{ uid }}" style="height: {{ height }};"></canvas>
7
+ {% if add_controls %}
8
+ <div id="controls_{{ uid }}" style="width: auto; margin-right: 10px;"></div>
9
+ {% endif %}
10
+ </div>
11
+
12
+ <script>
13
+ compare_view.load(
14
+ [{% for image_url in image_urls %}
15
+ "{{ image_url }}",
16
+ {% endfor %}],
17
+ "canvas_{{ uid }}",
18
+ {{config}}
19
+ );
20
+ </script>
@@ -0,0 +1,28 @@
1
+ [tool.poetry]
2
+ name = "jupyter_compare_view"
3
+ version = "0.2.5"
4
+ description = "Blend Between Multiple Images in JupyterLab."
5
+ authors = ["Octoframes"]
6
+ license = "MIT"
7
+ readme = "README.md"
8
+ include = ["vendor/compare_view/browser_compare_view.js"]
9
+
10
+ [tool.poetry.dependencies]
11
+ python = ">=3.8,<3.13"
12
+ ipython = ">=6.0.0"
13
+ ipykernel = ">=5.0.0"
14
+ Pillow = ">=7.1.2"
15
+ Jinja2 = ">=2.11.3"
16
+
17
+ [tool.poetry.dev-dependencies]
18
+ black = { extras = ["jupyter"], version = ">=22.3.0" }
19
+ matplotlib = ">=3.5.1"
20
+ scipy = ">=1.8.0"
21
+ scikit-image = ">=0.19.2"
22
+ pytest = ">=7.1.2"
23
+ isort = ">=5.10.1"
24
+ jupyterlab = ">=4.5.9"
25
+
26
+ [build-system]
27
+ requires = ["poetry-core>=1.0.0"]
28
+ build-backend = "poetry.core.masonry.api"
@@ -0,0 +1 @@
1
+ var compare_view;(()=>{"use strict";var e,t,c={d:(e,t)=>{for(var n in t)c.o(t,n)&&!c.o(e,n)&&Object.defineProperty(e,n,{enumerable:!0,get:t[n]})},o:(e,t)=>Object.prototype.hasOwnProperty.call(e,t),r:e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})}},n={};function o(e){var t,c;let n=null===(t=e[0])||void 0===t?void 0:t.element.width,o=null===(c=e[0])||void 0===c?void 0:c.element.height;for(let t of e)t.element.width==n&&t.element.height==o||console.log("Warning: images don't have the same resolution"),n=Math.max(n,t.element.width),o=Math.max(o,t.element.height);return[n,o]}function a(e){return e.images.unshift(e.images.pop()),!0}function i(e,c,n){c.onchange=()=>{c.checked?(e.next_mode=n,b(e,t.change_mode)):r(e)}}function r(t){var c;if(null!=t.ctrl_data)switch((c=t.ctrl_data).circle_check.checked=!1,c.horizontal_check.checked=!1,c.vertical_check.checked=!1,t.current_mode){case e.circle:t.ctrl_data.circle_check.checked=!0;break;case e.horizontal:t.ctrl_data.horizontal_check.checked=!0;break;case e.vertical:t.ctrl_data.vertical_check.checked=!0;break;default:throw`unsupported mode: ${t.current_mode}`}}function s(e,t){let c=e.canvas.getBoundingClientRect(),n=e.width/c.width,o=e.height/c.height;e.mouse_pos=[(t.clientX-c.left)*n,(t.clientY-c.top)*o]}function l(e){return e.render_circle=!0,e.canvas.style.cursor="none",!1}function u(e){return x(e,t.update_circle),e.canvas.style.cursor="default",!0}function d(e,t,c){e.ctx.beginPath(),e.ctx.arc(e.mouse_pos[0],e.mouse_pos[1],e.circle_size,t,c),c-t!=2*Math.PI&&e.ctx.lineTo(e.mouse_pos[0],e.mouse_pos[1]),e.ctx.closePath()}function _(e,t,c,n){var o;d(e,c,n),e.ctx.save(),e.ctx.clip(),e.ctx.drawImage(null===(o=e.images[t])||void 0===o?void 0:o.element,0,0,e.width,e.height),e.ctx.restore(),e.show_circle&&(d(e,c,n),e.ctx.strokeStyle="black",e.ctx.lineWidth=e.circumference_thickness,e.ctx.stroke())}function h(c,n){return b(c,t.update_slider),c.start_timestamp=n,c.start_pos=c.slider_pos,c.current_mode==e.horizontal?c.target_pos=c.mouse_pos[0]/c.width:c.target_pos=c.mouse_pos[1]/c.height,!0}function m(t,c){return t.held_down&&(t.current_mode==e.horizontal?t.target_pos=t.mouse_pos[0]/t.width:t.target_pos=t.mouse_pos[1]/t.height,t.start_pos=t.target_pos,t.slider_pos=t.target_pos),!0}function v(e,t){let c=(t-e.start_timestamp)/e.slider_time;return c=Math.min(Math.max(c,0),1),c=e.rate_function(c),e.slider_pos=(1-c)*e.start_pos+c*e.target_pos,e.slider_pos==e.target_pos}function p(c){switch(c.current_mode){case e.undefined:break;case e.circle:!function(e){e.canvas.onmousemove=null,e.canvas.onmouseleave=null,e.canvas.ontouchstart=null,e.canvas.onmousedown=null,e.canvas.onfocus=null,e.canvas.onblur=null,e.canvas.style.cursor="default",document.body.style.userSelect="text",e.touching=!1,b(e,t.remove_circle)}(c);break;case e.horizontal:case e.vertical:!function(e){e.canvas.onmousedown=null,e.canvas.onmouseup=null,document.onmouseup=null,e.canvas.onmouseup=null,e.canvas.onmousemove=null,e.canvas.onmouseenter=null,e.canvas.onmouseleave=null,e.canvas.onfocus=null,e.canvas.onblur=null,e.held_down=!1,e.canvas.style.cursor="default",document.body.style.userSelect="text"}(c);break;default:throw`unsupported mode: ${c.current_mode}`}}function g(c){switch(c.current_mode=c.next_mode,c.next_mode){case e.circle:!function(e){e.canvas.onmousemove=c=>{s(e,c),b(e,t.update_circle)},e.canvas.onmouseleave=()=>{b(e,t.remove_circle)},e.revolve_imgs_on_click&&(e.canvas.ontouchstart=()=>{e.touching=!0},e.canvas.onmousedown=()=>{e.touching||b(e,t.revolve_imgs)}),e.canvas.matches(":hover")&&b(e,t.update_circle),e.canvas.onfocus=()=>{document.body.style.userSelect="none"},e.canvas.onblur=()=>{document.body.style.userSelect="text",e.touching=!1}}(c);break;case e.horizontal:case e.vertical:!function(c){c.canvas.onmousedown=()=>{c.held_down=!0,b(c,t.start_slider_move)},c.canvas.onmouseup=()=>{c.held_down=!1},document.onmouseup=()=>{c.held_down=!1},c.canvas.onmousemove=e=>{s(c,e),b(c,t.possible_instant_slide)},c.current_mode==e.horizontal?c.canvas.onmouseenter=()=>{c.canvas.style.cursor="ew-resize"}:c.canvas.onmouseenter=()=>{c.canvas.style.cursor="ns-resize"},c.canvas.onmouseleave=()=>{c.canvas.style.cursor="default"},c.canvas.onfocus=()=>{document.body.style.userSelect="none"},c.canvas.onblur=()=>{document.body.style.userSelect="text"}}(c);break;default:throw`unsupported mode: ${c.current_mode}`}r(c)}function f(e){return p(e),g(e),!0}function w(c,n){!function(e,c){let n=[];for(;e.task_stack.length;){let o,i=e.task_stack.pop();switch(i){case t.none:o=!0;break;case t.change_mode:o=f(e);break;case t.revolve_imgs:o=a(e);break;case t.update_circle:o=l(e);break;case t.remove_circle:o=u(e);break;case t.start_slider_move:o=h(e,c);break;case t.possible_instant_slide:o=m(e);break;case t.update_slider:o=v(e,c);break;default:throw`unknown task: ${i}`}o||n.push(i)}e.task_stack=n}(c,n),function(t){switch(t.current_mode){case e.circle:!function(e){var t;if(e.render_circle){e.ctx.clearRect(0,0,e.width,e.height),function(e){var t;e.ctx.beginPath(),e.ctx.arc(e.mouse_pos[0],e.mouse_pos[1],e.circle_size-1,0,2*Math.PI),e.ctx.lineTo(e.width,0),e.ctx.lineTo(0,0),e.ctx.lineTo(0,e.height),e.ctx.lineTo(e.width,e.height),e.ctx.lineTo(e.width,0),e.ctx.closePath(),e.ctx.save(),e.ctx.clip(),e.ctx.drawImage(null===(t=e.images[0])||void 0===t?void 0:t.element,0,0,e.width,e.height),e.ctx.restore()}(e);for(let t=1;t<e.images_len;++t)_(e,t,(t-1)*Math.PI*2/(e.images_len-1),t*Math.PI*2/(e.images_len-1))}else e.ctx.clearRect(0,0,e.width,e.height),e.ctx.drawImage(null===(t=e.images[0])||void 0===t?void 0:t.element,0,0,e.width,e.height);e.render_circle=!1}(t);break;case e.horizontal:case e.vertical:!function(t){var c;t.ctx.clearRect(0,0,t.width,t.height),t.ctx.drawImage(null===(c=t.images[0])||void 0===c?void 0:c.element,0,0,t.width,t.height),t.show_slider&&function(t){t.ctx.beginPath(),t.current_mode==e.horizontal?(t.ctx.moveTo(t.slider_pos*t.width,0),t.ctx.lineTo(t.slider_pos*t.width,t.height)):(t.ctx.moveTo(0,t.slider_pos*t.height),t.ctx.lineTo(t.width,t.slider_pos*t.height)),t.ctx.closePath(),t.ctx.strokeStyle="black",t.ctx.lineWidth=t.slider_thickness,t.ctx.stroke()}(t),function(t){var c;t.ctx.beginPath(),t.current_mode==e.horizontal?t.ctx.rect(t.slider_pos*t.width,0,t.width,t.height):t.ctx.rect(0,t.slider_pos*t.height,t.width,t.height),t.ctx.closePath(),t.ctx.save(),t.ctx.clip(),t.ctx.clearRect(0,0,t.width,t.height),t.ctx.drawImage(null===(c=t.images[1])||void 0===c?void 0:c.element,0,0,t.width,t.height),t.ctx.restore()}(t)}(t);break;default:throw`unsupported mode: ${t.current_mode}`}}(c),c.task_stack.length?k(c):c.next_update_queued=!1}function k(e){e.next_update_queued=!0,window.requestAnimationFrame((t=>{w(e,t)}))}function x(e,t){let c=e.task_stack.indexOf(t);-1!=c&&e.task_stack.splice(c,1)}function b(e,t){x(e,t),e.task_stack.push(t),function(e){e.next_update_queued||k(e)}(e)}c.r(n),c.d(n,{load:()=>I}),function(e){e[void 0]="undefined",e.horizontal="horizontal",e.vertical="vertical",e.circle="circle"}(e||(e={})),function(e){e[e.none=0]="none",e[e.revolve_imgs=1]="revolve_imgs",e[e.change_mode=2]="change_mode",e[e.update_circle=3]="update_circle",e[e.remove_circle=4]="remove_circle",e[e.start_slider_move=5]="start_slider_move",e[e.possible_instant_slide=6]="possible_instant_slide",e[e.update_slider=7]="update_slider"}(t||(t={}));const y={start_mode:e.circle,circumference_fraction:.005,circle_size:void 0,circle_fraction:.2,show_circle:!0,revolve_imgs_on_click:!0,slider_fraction:.01,slider_time:400,rate_function:function(e){return e<.5?4*e*e*e:1-Math.pow(-2*e+2,3)/2},start_slider_pos:.5,show_slider:!0};function z(e,t){return null!=e[t]?e[t]:y[t]}function M(c,n,a,r){!function(e,t){if(e.length<2)throw`image_urls must contain at least two images, not ${e.length}`;let c=[],n=0,a=e.length;for(let i=0;i<a;++i){let r={url:e[i],element:document.createElement("img"),label:`${i}`};c.push(r),r.element.onload=()=>{++n,n==a&&t(c,o(c))},r.element.src=r.url}}(c,((c,o)=>{let s={images:c,images_len:c.length,canvas:n.canvas,ctx:n,width:0,height:0,ctrl_data:r,mouse_pos:[0,0],held_down:!1,next_mode:z(a,"start_mode"),current_mode:e.undefined,task_stack:[],next_update_queued:!1,circumference_thickness:0,render_circle:!1,circle_size:0,show_circle:z(a,"show_circle"),revolve_imgs_on_click:z(a,"revolve_imgs_on_click"),touching:!1,slider_thickness:0,slider_pos:z(a,"start_slider_pos"),slider_time:z(a,"slider_time"),rate_function:z(a,"rate_function"),show_slider:z(a,"show_slider"),start_timestamp:0,start_pos:0,target_pos:0};if(function(c){null!=c.ctrl_data&&(i(c,c.ctrl_data.circle_check,e.circle),i(c,c.ctrl_data.horizontal_check,e.horizontal),i(c,c.ctrl_data.vertical_check,e.vertical),c.ctrl_data.revolve_imgs_button.onclick=()=>{b(c,t.revolve_imgs)})}(s),function(e,t,c){e.canvas.width=t[0],e.canvas.height=t[1],e.width=e.canvas.width,e.height=e.canvas.height,function(e,t){let c=Math.max(e.canvas.width,e.canvas.height);e.circumference_thickness=c*z(t,"circumference_fraction"),e.circle_size=null!=t.circle_size?t.circle_size:c*z(t,"circle_fraction"),e.slider_thickness=c*z(t,"slider_fraction")}(e,c)}(s,o,a),"y"===s.canvas.dataset.in_use)throw`the canvas with the id '${s.canvas.id}' is already in use`;s.canvas.dataset.in_use="y",b(s,t.change_mode)}))}function P(e,t,c){let n=document.createElement("input");n.type="checkbox",n.id=e;let o=document.createElement("label");o.innerHTML=t,o.htmlFor=n.id;let a=document.createElement("br");return c.appendChild(n),c.appendChild(o),c.appendChild(a),n}function T(e,t){let c=document.createElement("button");c.innerHTML=e;let n=document.createElement("br");return t.appendChild(c),t.appendChild(n),c}function I(e,t,c={}){M(e,function(e){let t=document.getElementById(e),c=t.getContext("2d");return t.tabIndex=1,c}(t),c,null!=c.controls_id?function(e,t=function(){let e="";for(let t=0;t<12;t++)e+="ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789".charAt(Math.floor(36*Math.random()));return e}()){let c=document.getElementById(e);if(null==c)throw`controls_id '${e}' isn't valid`;return{controls_parent:c,circle_check:P(`${t}_circle_button`,"Circle",c),horizontal_check:P(`${t}_horizontal_button`,"Horizontal",c),vertical_check:P(`${t}_vertical_button`,"Vertical",c),revolve_imgs_button:T("Revolve Images",c)}}(c.controls_id,c.key):void 0)}compare_view=n})();