Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
19 changes: 19 additions & 0 deletions .github/workflows/python-package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,21 +19,40 @@ jobs:

steps:
- uses: actions/checkout@v2

- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}

- name: Install wkhtmltopdf
run: |
if [ "$RUNNER_OS" == "Linux" ]; then
# apt install important_linux_software
sudo apt-get install xvfb libfontconfig wkhtmltopdf
elif [ "$RUNNER_OS" == "Windows" ]; then
choco install wkhtmltopdf
elif [ "$RUNNER_OS" == "macOS" ]; then
brew cask install wkhtmltopdf
else
echo "$RUNNER_OS not supported"
exit 1
fi
shell: bash

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install flake8 pytest
pip install -r requirements.txt

- name: Lint with flake8
run: |
# stop the build if there are Python syntax errors or undefined names
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics

- name: Test with pytest
run: |
pytest
61 changes: 50 additions & 11 deletions ipyplot/_html_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,53 @@
required for displaying images, grid/tab layout and general styling.
"""

import os
from typing import Sequence

import imgkit
import numpy as np
import shortuuid
from numpy import str_

from ._img_helpers import _img_to_base64
from ._utils import (
_find_and_replace_html_for_imgkit,
_to_imgkit_path)

try:
from IPython.display import display, HTML
except Exception: # pragma: no cover
raise Exception('IPython not detected. Plotting without IPython is not possible') # NOQA E501


def _html_to_image(html, out_img):
if os.path.dirname(out_img) != '':
os.makedirs(os.path.dirname(out_img), exist_ok=True)

html = _find_and_replace_html_for_imgkit(html)
options = {
# 'xvfb': '',
'enable-local-file-access': '',
}
saving = True
while saving:
print("Saving output as image under: ", out_img)

try:
# path_wkthmltoimage = r'C:/Program Files/wkhtmltopdf/bin/wkhtmltoimage.exe'
# config = imgkit.config(wkhtmltoimage=path_wkthmltoimage)
imgkit.from_string(
html, out_img,
# config=config,
options=options)
except Exception as e:
if "You need to install xvfb" in str(e):
options['xvfb'] = ''
continue
raise
saving = False


def _create_tabs(
images: Sequence[object],
labels: Sequence[str or int],
Expand All @@ -25,7 +58,7 @@ def _create_tabs(
img_width: int = 150,
zoom_scale: float = 2.5,
force_b64: bool = False,
tabs_order: Sequence[str or int] = None):
tabs_order: Sequence[str or int] = None) -> str:
"""
Generates HTML code required to display images in interactive tabs grouped by labels.
For tabs ordering and filtering check out `tabs_order` param.
Expand Down Expand Up @@ -63,6 +96,11 @@ def _create_tabs(
By default, tabs will be sorted alphabetically based on provided labels.
This param can be also used as a filtering mechanism - only labels provided in `tabs_order` param will be displayed as tabs.
Defaults to None.

Returns
-------
str
HTML code for class tabs viewer control.
""" # NOQA E501

tab_layout_id = shortuuid.uuid()
Expand Down Expand Up @@ -144,7 +182,7 @@ def _create_tabs(


def _create_html_viewer(
html: str):
html: str) -> str:
"""Creates HTML code for HTML previewer.

Parameters
Expand Down Expand Up @@ -235,7 +273,7 @@ def _create_img(
width: int,
grid_style_uuid: str,
custom_text: str = None,
force_b64: bool = False):
force_b64: bool = False) -> str:
"""Helper function to generate HTML code for displaying images along with corresponding texts.

Parameters
Expand Down Expand Up @@ -272,11 +310,14 @@ def _create_img(
use_b64 = True
# if image is a string (URL) display its URL
if type(image) is str or type(image) is str_:
img_html += '<h4 style="font-size: 9px; padding-left: 10px; padding-right: 10px; width: 95%%; word-wrap: break-word; white-space: normal;">%s</h4>' % (image) # NOQA E501
matches = ['http:', 'https:', 'ftp:', 'www.']
if not any(image.lower().startswith(x) for x in matches):
image = os.path.relpath(image)
img_html += '<h4 style="font-size: 9px; margin-left: 5px; margin-right: 5px; word-wrap: break-word; white-space: normal;">%s</h4>\n' % (image) # NOQA E501
if not force_b64:
use_b64 = False
img_html += '<img src="%s"/>' % image
elif "http" in image:
elif any(image.lower().startswith(x) for x in matches):
print("WARNING: Current implementation doesn't allow to use 'force_b64=True' with images as remote URLs. Ignoring 'force_b64' flag") # NOQA E501
use_b64 = False

Expand Down Expand Up @@ -309,7 +350,7 @@ def _create_imgs_grid(
max_images: int = 30,
img_width: int = 150,
zoom_scale: float = 2.5,
force_b64: bool = False):
force_b64: bool = False) -> str:
"""
Creates HTML code for displaying images provided in `images` param in grid-like layout.
Check optional params for max number of images to plot, labels and custom texts to add to each image, image width and other options.
Expand Down Expand Up @@ -355,7 +396,7 @@ def _create_imgs_grid(
# create code with style definitions
html, grid_style_uuid = _get_default_style(img_width, zoom_scale)

html += '<div id="ipyplot-imgs-container-div-%s">' % grid_style_uuid
html += '<div class="ipyplot-imgs-container-div-%s">' % grid_style_uuid
html += ''.join([
_create_img(
x, width=img_width, label=y,
Expand All @@ -370,7 +411,7 @@ def _create_imgs_grid(
return html


def _get_default_style(img_width: int, zoom_scale: float):
def _get_default_style(img_width: int, zoom_scale: float) -> str:
"""Creates HTML code with default style definitions required for elements to be properly displayed

Parameters
Expand All @@ -389,13 +430,11 @@ def _get_default_style(img_width: int, zoom_scale: float):
style_uuid = shortuuid.uuid()
html = """
<style>
#ipyplot-imgs-container-div-%(0)s {
div.ipyplot-imgs-container-div-%(0)s {
width: 100%%;
height: 100%%;
margin: 0%%;
overflow: auto;
position: relative;
overflow-y: scroll;
}

div.ipyplot-placeholder-div-%(0)s {
Expand Down
6 changes: 4 additions & 2 deletions ipyplot/_img_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,9 @@ def _rescale_to_width(
return rescaled_img


def _scale_wh_by_target_width(w: int, h: int, target_width: int):
def _scale_wh_by_target_width(
w: int, h: int,
target_width: int) -> (int, int):
"""Helper functions for scaling width and height based on target width.

Parameters
Expand All @@ -56,7 +58,7 @@ def _scale_wh_by_target_width(w: int, h: int, target_width: int):

def _img_to_base64(
image: str or str_ or np.ndarray or PIL.Image,
target_width: int = None):
target_width: int = None) -> str:
"""Converts image to base64 string.
Use `target_width` param to rescale the image to specific width - keeps original size by default.

Expand Down
14 changes: 10 additions & 4 deletions ipyplot/_plotting.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from typing import Sequence

from ._html_helpers import (
_display_html, _create_tabs, _create_imgs_grid)
_display_html, _create_tabs, _create_imgs_grid, _html_to_image)
from ._utils import _get_class_representations, _seq2arr


Expand Down Expand Up @@ -88,7 +88,8 @@ def plot_images(
max_images: int = 30,
img_width: int = 150,
zoom_scale: float = 2.5,
force_b64: bool = False):
force_b64: bool = False,
output_img_path: str = None):
"""
Simply displays images provided in `images` param in grid-like layout.
Check optional params for max number of images to plot, labels and custom texts to add to each image, image width and other options.
Expand Down Expand Up @@ -143,6 +144,9 @@ def plot_images(
zoom_scale=zoom_scale,
force_b64=force_b64)

if output_img_path is not None:
_html_to_image(html, output_img_path)

_display_html(html)


Expand All @@ -153,7 +157,8 @@ def plot_class_representations(
zoom_scale: float = 2.5,
force_b64: bool = False,
ignore_labels: Sequence[str or int] = None,
labels_order: Sequence[str or int] = None):
labels_order: Sequence[str or int] = None,
output_img_path: str = None):
"""
Displays single image (first occurence for each class) for each label/class in grid-like layout.
Check optional params for labels filtering, ignoring and ordering, image width and other options.
Expand Down Expand Up @@ -208,4 +213,5 @@ def plot_class_representations(
max_images=len(images),
img_width=img_width,
zoom_scale=zoom_scale,
force_b64=force_b64)
force_b64=force_b64,
output_img_path=output_img_path)
25 changes: 22 additions & 3 deletions ipyplot/_utils.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,37 @@
"""
Misc utils for IPyPlot package.
"""

import os
import re
from typing import Sequence

import numpy as np
from PIL import Image


def _to_imgkit_path(img_path: str) -> str:
# matches = ['http:', 'https:', 'ftp:', 'www.']
# if not any(x in img_path for x in matches):
# if '.' in img_path:
return "file:///" + os.path.abspath(img_path)
# return img_path


def _find_and_replace_html_for_imgkit(html: str) -> str:
# pattern = r"<img src=\"[/\.].*\""
pattern = r"<img src=\"(?!http:|https:|data:|www.|ftp:|ftps:).*\""
return re.sub(
pattern,
lambda x: '<img src="%s"' % _to_imgkit_path(
x.group().split('="')[1].replace('"', '')),
html)


def _get_class_representations(
images: Sequence[object],
labels: Sequence[str or int],
ignore_labels: Sequence[str or int] = None,
labels_order: Sequence[str or int] = None):
labels_order: Sequence[str or int] = None) -> (np.ndarray, np.ndarray):
"""Returns a list of images (and labels) representing first occurance of each label/class type.
Check optional params for labels ignoring and ordering.
For labels filtering refer to `labels_order` param.
Expand Down Expand Up @@ -77,7 +96,7 @@ def _get_class_representations(
return out_images, out_labels


def _seq2arr(seq: Sequence[str or int or object]):
def _seq2arr(seq: Sequence[str or int or object]) -> np.ndarray:
"""Convert sequence to numpy.ndarray.

Parameters
Expand Down
3 changes: 2 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ bump2version
pytest
pytest-cov
shortuuid
pandas
pandas
imgkit
Binary file added tests/data/100205.jpeg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added tests/data/21HnHt+LMDL._AC_US436_QL65_.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added tests/data/41edw+BCUjL._AC_US436_QL65_.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added tests/data/out_img.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added tests/data/out_img_b64.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading