Skip to content

Commit

Permalink
Image rendering support in pyecharts 1.0.0 (pyecharts#973)
Browse files Browse the repository at this point in the history
* ✨ add safari support. pyecharts#970, pyecharts#972

* 🐛 fix html file removing bug and enable google chrome as a driver on mac os

* 🐛 non-negative value is not allowed

* Update: remove async js code

* 🔨 merge with pyecharts-snapshot. ✨ initial prototype

* 🔥 remove unwanted code and remove dependency on PIL

* ✨ passing on addtional parameters

* ✨ register additional packages

* 🔥 remove snapshot command

* 🔨 fix typo

* 💄 workout type hints

* 🐛 json.dumps cannot serialize timestamp, datetime and date

* 🔥 remove useless function

* Update: make snapshot logic clear
  • Loading branch information
chfw authored Apr 10, 2019
1 parent 2fa36bc commit a393908
Show file tree
Hide file tree
Showing 5 changed files with 94 additions and 75 deletions.
8 changes: 7 additions & 1 deletion pyecharts/charts/base.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# coding=utf-8
import datetime
import json
import os
import uuid
Expand Down Expand Up @@ -44,7 +45,7 @@ def get_options(self) -> dict:
return utils.remove_key_with_none_value(self.options)

def dump_options(self) -> str:
return json.dumps(self.get_options(), indent=4)
return json.dumps(self.get_options(), indent=4, default=default)

def render(
self,
Expand Down Expand Up @@ -96,3 +97,8 @@ def load_javascript(self):
f, ext = FILENAMES[dep]
scripts.append("{}{}.{}".format(CurrentConfig.ONLINE_HOST, f, ext))
return Javascript(lib=scripts)


def default(o):
if isinstance(o, (datetime.date, datetime.datetime)):
return o.isoformat()
6 changes: 3 additions & 3 deletions pyecharts/options/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@
RadiusAxisOpts,
SingleAxisOpts,
TitleOpts,
ToolboxOpts,
ToolBoxFeatureOpts,
ToolboxOpts,
TooltipOpts,
VisualMapOpts,
)
Expand All @@ -35,12 +35,12 @@
ItemStyleOpts,
LabelOpts,
LineStyleOpts,
MarkAreaItem,
MarkAreaOpts,
MarkLineItem,
MarkLineOpts,
MarkPointItem,
MarkPointOpts,
MarkAreaItem,
MarkAreaOpts,
SplitAreaOpts,
SplitLineOpts,
TextStyleOpts,
Expand Down
2 changes: 1 addition & 1 deletion pyecharts/options/series_options.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# coding=utf-8
from ..commons.types import List, Numeric, Optional, Sequence, Union, Tuple
from ..commons.types import List, Numeric, Optional, Sequence, Tuple, Union


class ItemStyleOpts:
Expand Down
147 changes: 77 additions & 70 deletions pyecharts/render/snapshot.py
Original file line number Diff line number Diff line change
@@ -1,84 +1,91 @@
# coding=utf-8
import base64
import time
import sys
import codecs
import logging
import os
from io import BytesIO

from selenium import webdriver
from selenium.common import exceptions
from selenium.webdriver.common.desired_capabilities import DesiredCapabilities
from ..commons.types import Any

logger = logging.getLogger(__name__)

PNG_FORMAT = "png"
JPG_FORMAT = "jpeg"
GIF_FORMAT = "gif"
PDF_FORMAT = "pdf"
SVG_FORMAT = "svg"
EPS_FORMAT = "eps"
B64_FORMAT = "base64"


def make_snapshot(
html_path: str,
image_name: str,
driver: Any,
file_name: str,
output_name: str,
delay: float = 2,
pixel_ratio: int = 2,
delay: int = 2,
is_remove_html: bool = True,
browser='Chrome'
is_remove_html: bool = False,
**kwargs,
):
if delay < 0:
raise Exception('Time travel is not possible')
if browser == 'Chrome':
driver = get_chrome()
elif browser == 'Safari':
driver = get_safari()
logger.info("Generating file ...")
file_type = output_name.split(".")[-1]

content = driver.make_snapshot(file_name, file_type, delay, pixel_ratio, **kwargs)
if file_type in [SVG_FORMAT, B64_FORMAT]:
save_as_text(content, output_name)
else:
raise Exception('Unknown browser!')
driver.set_script_timeout(delay + 1)
# pdf, gif, png, jpeg
content_array = content.split(",")
if len(content_array) != 2:
raise OSError(content_array)

if not html_path.startswith("http"):
html_path = 'file://' + os.path.abspath(html_path)
driver.get(html_path)
time.sleep(delay)
image_data = decode_base64(content_array[1])

ext = image_name.split(".")[1]
if file_type in [PDF_FORMAT, GIF_FORMAT, EPS_FORMAT]:
save_as(image_data, output_name, file_type)
elif file_type in [PNG_FORMAT, JPG_FORMAT]:
save_as_png(image_data, output_name)
else:
raise TypeError("Not supported file type '%s'".format(file_type))

try:
output = driver.execute_script(__gen_js_code(ext, pixel_ratio, delay))
except exceptions.TimeoutException:
pass
if "/" not in output_name:
output_name = os.path.join(os.getcwd(), output_name)

if is_remove_html and not file_name.startswith("http"):
os.unlink(file_name)
logger.info("File saved in %s" % output_name)


def decode_base64(data: str) -> bytes:
"""Decode base64, padding being optional.
:param data: Base64 data as an ASCII byte string
:returns: The decoded byte string.
"""
missing_padding = len(data) % 4
if missing_padding != 0:
data += "=" * (4 - missing_padding)
return base64.decodebytes(data.encode("utf-8"))


def save_as_png(image_data: bytes, output_name: str):
with open(output_name, "wb") as f:
f.write(image_data)


def save_as_text(image_data: str, output_name: str):
with codecs.open(output_name, "w", encoding="utf-8") as f:
f.write(image_data)


def save_as(image_data: bytes, output_name: str, file_type: str):
try:
output = output.split(",")[1]
except:
raise

with open(image_name, "wb") as f:
f.write(base64.b64decode(output))

if is_remove_html and not html_path.startswith("http"):
os.remove(html_path[7:])
driver.close()
print("render {}".format(image_name))


def __gen_js_code(file_type: str, pixel_ratio: int, delay: int) -> str:
script = (
"""
var ele = document.querySelector('div[_echarts_instance_]');
var mychart = echarts.getInstanceByDom(ele);
return mychart.getDataURL(
{type:'--file-type--', pixelRatio: --pixel-ratio--, excludeComponents: ['toolbox']});
""".replace(
"--file-type--", file_type
)
.replace("--pixel-ratio--", str(pixel_ratio))
)
return script


def get_chrome():
option = webdriver.ChromeOptions()
option.add_argument("headless")
if sys.platform == 'darwin':
option.binary_location = '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'
capabilities = DesiredCapabilities.CHROME
capabilities["loggingPrefs"] = {"browser": "ALL"}
return webdriver.Chrome(
options=option,
desired_capabilities=capabilities)


def get_safari():
return webdriver.Safari(executable_path='/usr/bin/safaridriver')
from PIL import Image

m = Image.open(BytesIO(image_data))
m.load()
color = (255, 255, 255)
b = Image.new("RGB", m.size, color)
b.paste(m, mask=m.split()[3])
b.save(output_name, file_type, quality=100)
except ModuleNotFoundError:
raise Exception("Please install PIL for % image type" % file_type)
6 changes: 6 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@
__license__ = "MIT"

__requires__ = ["jinja2", "prettytable"]
__extra_requires__ = {
"selenium": ["snapshot-selenium"],
"phantomjs": ["snapshot-phantomjs"],
"images": ["PIL"],
}

__keywords__ = ["Echarts", "charts", "plotting-tool"]
# Load the package's _version.py module as a dictionary.
Expand Down Expand Up @@ -95,4 +100,5 @@ def run(self):
"Topic :: Software Development :: Libraries",
],
cmdclass={"upload": UploadCommand},
extras_require=__extra_requires__,
)

0 comments on commit a393908

Please sign in to comment.