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

compare_view widget and colab support #41

Merged
merged 30 commits into from
Dec 9, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
9bfac7a
Rename inject.py to compare.py
amorgun Nov 19, 2022
26e467c
Delete inject_dependencies.html
amorgun Nov 19, 2022
7988faf
Rename inject_split.html to template.html
amorgun Nov 19, 2022
8ed44d6
Update __init__.py
amorgun Nov 19, 2022
773c77b
Update compare.py
amorgun Nov 19, 2022
c6bce85
Update template.html
amorgun Nov 19, 2022
22452c8
Update sw_cellmagic.py
amorgun Nov 19, 2022
5303710
Update pyproject.toml
amorgun Nov 19, 2022
c771cd2
Update compare.py
amorgun Nov 19, 2022
6745a91
Update compare.py
amorgun Nov 19, 2022
f73dbd0
Update compare.py
amorgun Nov 19, 2022
e6f8f2d
Update compare.py
amorgun Nov 19, 2022
2adac04
Update sw_cellmagic.py
amorgun Nov 19, 2022
20bcd04
Update sw_cellmagic.py
amorgun Nov 19, 2022
ff096d7
Update sw_cellmagic.py
amorgun Nov 19, 2022
875b56d
Update sw_cellmagic.py
amorgun Nov 19, 2022
d1b059f
Update sw_cellmagic.py
amorgun Nov 19, 2022
7c86b11
Update compare.py
amorgun Nov 19, 2022
d48c00a
Update sw_cellmagic.py
amorgun Nov 19, 2022
924c3ab
Update compare.py
amorgun Nov 19, 2022
3cfc608
Merge branch 'main' into main
amorgun Nov 20, 2022
85a7c8f
Update pyproject.toml
amorgun Nov 20, 2022
694baee
Update compare.py
amorgun Nov 20, 2022
b335c0a
Fix pr suggestions (#3)
amorgun Dec 3, 2022
4f97c37
Update compare.py
amorgun Dec 3, 2022
a343e7a
Update compare.py
amorgun Dec 4, 2022
c940605
Update sw_cellmagic.py
amorgun Dec 4, 2022
72d5ae5
Fix cell magic in Jupyter (#4)
amorgun Dec 5, 2022
85ff52f
Update pyproject.toml
amorgun Dec 6, 2022
3256aaf
Fix pr issues (#5)
amorgun Dec 6, 2022
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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

[![JupyterLight](https://jupyterlite.rtfd.io/en/latest/_static/badge.svg)](https://octoframes.github.io/jupyter_compare_view)
[![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/Octoframes/jupyter_compare_view/HEAD?labpath=example_notebook.ipynb)
[![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)
[![PyPI version](https://badge.fury.io/py/jupyter_compare_view.svg)](https://badge.fury.io/py/jupyter_compare_view)
[![MIT](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/Octoframes/jupyter_compare_view/blob/main/LICENSE)

Expand Down
821 changes: 347 additions & 474 deletions example_notebook.ipynb

Large diffs are not rendered by default.

13 changes: 3 additions & 10 deletions jupyter_compare_view/__init__.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,14 @@
from .sw_cellmagic import CompareViewMagic
from IPython import get_ipython
import pkg_resources

from .compare import inject_dependencies
from .compare import compare, StartMode
from .sw_cellmagic import CompareViewMagic

__version__: str = pkg_resources.get_distribution(__name__).version

print(f"Jupyter compare_view v{__version__}")

try:
ipy = get_ipython()
ipy.register_magics(CompareViewMagic)

inject_dependencies()


print(f"Jupyter compare_view v{__version__}")
except AttributeError:
print("Can not load CompareViewMagic because this is not a notebook")


147 changes: 115 additions & 32 deletions jupyter_compare_view/compare.py
Original file line number Diff line number Diff line change
@@ -1,48 +1,131 @@
import base64
import enum
import io
import json
import os
import typing
import uuid
from pathlib import Path
from jinja2 import Template, StrictUndefined
from IPython.core.display import HTML, JSON
from IPython.display import display
import IPython
import PIL


ImageLike = typing.TypeVar('ImageLike')
ImageSource = typing.Union[str, bytes, ImageLike]


def img2bytes(img: ImageLike, format: str, cmap: str) -> bytes:
with io.BytesIO() as im_file:
if isinstance(img, PIL.Image.Image):
img.save(im_file, format=format)
else:
# anything other that can be displayed with plt.imshow
import matplotlib.pyplot as plt

plt.imsave(im_file, img, format=format, cmap=cmap)
return im_file.getvalue()


def img2url(img: ImageSource, format: str, cmap: str) -> str:
if isinstance(img, str):
return img.strip()
if isinstance(img, bytes):
data = img
else:
data = img2bytes(img, format=format, cmap=cmap)
return f"data:image/{format};base64,{str(base64.b64encode(data), 'utf8')}"


def compile_template(in_file: str, **variables) -> str:
with open(in_file, "r", encoding="utf-8") as file:
template = Template(file.read(), undefined=StrictUndefined)
return template.render(**variables)


# injection is used in "" string in JavaScript -> some characters need to be escaped
def sanitise_injection(inject: str) -> str:
return inject.replace("\\", "\\\\").replace("\"", "\\\"").replace("\n", "\\n")
def prepare_html(image_urls: typing.List[str], height: str, add_controls: bool, config: dict) -> str:
uid=uuid.uuid1()
config['key'] = str(uid)
if add_controls:
config["controls_id"] = f"controls_{uid}"
root = Path(__file__).parent
js_path = root / "../vendor/compare_view/browser_compare_view.js"
js = js_path.read_text()
amorgun marked this conversation as resolved.
Show resolved Hide resolved
return compile_template(
root / "template.html",
uid=uid,
image_urls=image_urls,
height=height,
js=js,
add_controls=add_controls,
config=json.dumps(config),
)


def inject_dependencies() -> None:
js_path = Path(__file__).parents[1] / "vendor/compare_view/browser_compare_view.js"
js = sanitise_injection(js_path.read_text())
@enum.unique
class StartMode(str, enum.Enum):
CIRCLE = "circle"
HORIZONTAL = "horizontal"
VERTICAL = "vertical"

html_code = compile_template(
os.path.join((os.path.dirname(__file__)), "inject_dependencies.html"),
js=js,
)
display(HTML(html_code))


def inject_split(image_urls, height, config) -> None:
key=uuid.uuid1()
# inject controls id and key -> only Config remaining, not BrowserConfig for compare_view
# TODO: come up with better solution
config_parsed = json.loads(config.strip("'").strip('"'))
config_parsed["controls_id"] = f"controls_{key}"
config_parsed["key"] = str(key)
html_code = compile_template(
os.path.join((os.path.dirname(__file__)), "template.html"),
key=key,

def compare(
kolibril13 marked this conversation as resolved.
Show resolved Hide resolved
image1: ImageSource,
image2: ImageSource,
*other_images: ImageSource,
height: typing.Union[str, int] = 'auto',
add_controls: bool = True,
start_mode: typing.Union[StartMode, str] = StartMode.CIRCLE,
circumference_fraction: float = 0.005,
circle_size: typing.Optional[float] = None,
circle_fraction: float = 0.2,
show_circle: bool = True,
revolve_imgs_on_click: bool = True,
slider_fraction: float = 0.01,
slider_time: float = 400,
# rate_function: str = 'ease_in_out_cubic',
start_slider_pos: float = 0.5,
show_slider: bool = True,
display_format: str = 'jpeg',
cmap: typing.Optional[str] = None,
amorgun marked this conversation as resolved.
Show resolved Hide resolved
) -> IPython.display.HTML:
"""
Args:
height: height of the widget in pixels or "auto"
add_controls: pass False to not create controls
start_mode: either "circle", "horizontal" or "vertical"
circumference_fraction: size of circle outline as fraction of image width or height (whatever is bigger)
circle_size: the radius in pixel
circle_fraction: a fraction of the image width or height (whichever is bigger—called max_size in this document)
show_circle: draw line around circle
slider_time: time slider takes to reach clicked location
start_slider_pos: 0.0 -> left; 1.0 -> right
show_slider: draw line at slider
display_format: format used for displaying images
cmap: colormap for grayscale images
"""
images = [image1, image2, *other_images]
image_urls = [
img2url(img, format=display_format, cmap=cmap) for img in images
]
_locals = locals()
config = {k: _locals[k] for k in [
'start_mode',
'circumference_fraction',
'circle_fraction',
'show_circle',
'revolve_imgs_on_click',
'slider_fraction',
'slider_time',
# 'rate_function',
'start_slider_pos',
'show_slider',
]
+ ['circle_size'] * (circle_size is not None)
}
html = prepare_html(
image_urls=image_urls,
height=height,
config=json.dumps(config_parsed),
height=f'{height}px' if not isinstance(height, str) else height,
add_controls=add_controls,
config=config,
)
display(HTML(html_code))
# ensure to include the sources every time
inject_dependencies()

return IPython.display.HTML(html)
39 changes: 0 additions & 39 deletions jupyter_compare_view/inject_dependencies.html

This file was deleted.

29 changes: 10 additions & 19 deletions jupyter_compare_view/sw_cellmagic.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import io
from base64 import b64decode
import json

from IPython.core import magic_arguments
from IPython.core.magic import Magics, cell_magic, magics_class
from IPython.utils.capture import capture_output
from PIL import Image

from .compare import inject_split
from .compare import compare


@magics_class
Expand Down Expand Up @@ -48,6 +48,8 @@ def compare(self, line, cell): # TODO: make a %%splity deprecated version
data = output.data
if "image/png" in data:
png_bytes_data = data["image/png"]
if isinstance(png_bytes_data, str):
png_bytes_data = f'data:image/png;base64,{png_bytes_data}'
out_images_base64.append(png_bytes_data)
if len(out_images_base64) < 2:
raise ValueError(
Expand All @@ -56,25 +58,14 @@ def compare(self, line, cell): # TODO: make a %%splity deprecated version

# get the parameters that configure the widget
args = magic_arguments.parse_argstring(CompareViewMagic.compare, line)

height = args.height

if height == "auto":
imgdata = b64decode(out_images_base64[0])
# maybe possible without the PIL dependency?
im = Image.open(io.BytesIO(imgdata))
height = im.size[1]

image_data_urls = [
f"data:image/jpeg;base64,{base64.strip()}" for base64 in out_images_base64
]

# every juxtapose html node needs unique id
inject_split(
image_urls=image_data_urls,
height=height,
# as JSON object
config=args.config,
return compare(
*out_images_base64,
**{
**json.loads(args.config.strip("'").strip('"')),
"height": height if height == "auto" else int(height)
}
)

@cell_magic
Expand Down
15 changes: 10 additions & 5 deletions jupyter_compare_view/template.html
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
<script>
{{ js }}
</script>

<div style="display: flex; flex-direction: row; width: 100%;">
<canvas id="canvas_{{ key }}" style="height: {{ height }}px;"></canvas>
<div id="controls_{{ key }}" style="width: auto; margin-right: 10px;">
</div>
<canvas id="canvas_{{ uid }}" style="height: {{ height }};"></canvas>
{% if add_controls %}
<div id="controls_{{ uid }}" style="width: auto; margin-right: 10px;"></div>
{% endif %}
</div>

<script>
compare_view.load(
[{% for image_url in image_urls %}
"{{ image_url }}",
{% endfor %}],
"canvas_{{ key }}",
"canvas_{{ uid }}",
{{config}}
);
</script>
</script>
14 changes: 13 additions & 1 deletion jupyterlite_compare_view_notebook.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
"metadata": {},
"outputs": [],
"source": [
"%%compare --height auto\n",
"%%compare\n",
"\n",
"img = data.chelsea()\n",
"grayscale_img = rgb2gray(img)\n",
Expand All @@ -60,6 +60,18 @@
"plt.show() # only needed in JupyterLite"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "6b7ae2ae",
"metadata": {},
"outputs": [],
"source": [
"from jupyter_compare_view import compare\n",
"\n",
"compare(img, grayscale_img, add_controls=False, cmap=\"gray\")"
]
},
{
"cell_type": "code",
"execution_count": null,
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "jupyter_compare_view"
version = "0.1.6"
version = "0.2.0"
description = "Blend Between Multiple Images in JupyterLab."
authors = ["Octoframes"]
license = "MIT"
Expand Down