Skip to content

Commit

Permalink
compare_view widget and colab support (#41)
Browse files Browse the repository at this point in the history
  • Loading branch information
amorgun authored Dec 9, 2022
1 parent 53656f7 commit 3606514
Show file tree
Hide file tree
Showing 9 changed files with 500 additions and 581 deletions.
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()
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(
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,
) -> 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

0 comments on commit 3606514

Please sign in to comment.