Repository: bokeh/bokeh

Issue:
Exporting DataTable to png broken
Using bokeh master it appears that exporting a DataTable to png is broken. This appears to be a regression in 0.12.16 as it's working in 0.12.15. Here is a minimal reproducible example:

```python
from bokeh.io import export_png
from datetime import date
from random import randint

from bokeh.io import output_file, show
from bokeh.layouts import widgetbox
from bokeh.models import ColumnDataSource
from bokeh.models.widgets import DataTable, DateFormatter, TableColumn

output_file("data_table.html")

data = dict(
        dates=[date(2014, 3, i+1) for i in range(10)],
        downloads=[randint(0, 100) for i in range(10)],
    )
source = ColumnDataSource(data)

columns = [
        TableColumn(field="dates", title="Date", formatter=DateFormatter()),
        TableColumn(field="downloads", title="Downloads"),
    ]
data_table = DataTable(source=source, columns=columns, width=400, height=280)

export_png(data_table, filename="table.png")
```

>UnicodeEncodeError: 'latin-1' codec can't encode character '\ufffd' in position 879408: ordinal not in range(256)


Discussion:
Can you narrow it down to a last working dev build?
Also, what version of Python and what platform do you use? I can't reproduce anything in Python 2/3 on Linux. Additionally, can you show the full traceback. Having only the last bit doesn't really help with determining what's the origin of this error.
Actually, I can reproduce on Python 3, just with development mode disabled (I presume inline resources are the problem):
```
Traceback (most recent call last):
  File "some17.py", line 22, in <module>
    export_png(data_table)
  File "/home/mateusz/repos/bokeh/bokeh/io/export.py", line 80, in export_png
    image = get_screenshot_as_png(obj, height=height, width=width, driver=webdriver)
  File "/home/mateusz/repos/bokeh/bokeh/io/export.py", line 175, in get_screenshot_as_png
    file.write(b(html))
  File "/home/mateusz/miniconda/envs/bk3/lib/python3.6/site-packages/six.py", line 626, in b
    return s.encode("latin-1")
UnicodeEncodeError: 'latin-1' codec can't encode character '\ufffd' in position 879897: ordinal not in range(256
```
Digging a bit in embed code, I realized that `html_file()` used in `export_png()` produces a UTF-8 encoded string, well sort of due to this function:

https://github.com/bokeh/bokeh/blob/fa1face4fe5f83d835436ffddc96ed62ba67b4a9/bokeh/util/string.py#L8-L21

This function either does a transform `unicode -> str` on Python 2 or `str -> str` on Python 3. It should, however, do `str -> bytes` transform on Python 3. What am I missing, because this seems obviously wrong and then causes all sorts of issues down the pipe?

If remove encoding from `file_html()` (`html_page_for_render_items()` to be specific) and then use `f = io.open(filename, mode="w", encoding="utf-8"); f.write(html)` for saving generated HTML, then this works reliably on both Python 2 and 3.
Well that's weird, I am willing to believe the docstring is wrong. Regarding the  function itself, I'm not sure, it has been in use for years. If there's something better to do, great, we just need to be careful. 
> Regarding the function itself, I'm not sure, it has been in use for years.

bokeh.embed, bokeh.io and bokeh.server heavily depend on it. So far I removed 15 instances for the purpose of testing alternative approaches.
Sorry for the incomplete traceback and thanks for tracking down the issue. I'm not sure if it's related but the escaping of the INLINE resources also seems to have changed, seems like that could at least in theory account for the differences.
> but the escaping of the INLINE resources also seems to have changed

Can you elaborate? I can imagine a few ways to interpret this
FTW if you have time, I *do* think a bisection would help inform the appropriate immediate-term response 
I'm still tracking it down, but when I build the holoviews docs using bokeh master I'm currently seeing the INLINE JS appearing unescaped, which appears to break things completely, e.g. I'm seeing HTML less than (``&lt;``) or unescaped quotes show up, which means the JS won't parse. We're already switching to a better system which doesn't have these issue I think, so it's not critical, but something does appear to have changed.
@philippjfr that actually seems very worrisome, if you can help track it down I think that would be very valuable
>I do think a bisection would help inform the appropriate immediate-term response

I'll try to track it down.
@philippjfr Can you detail the process to evaluate builds for this difference? I'd like to do a bisection today, but I need to know what to look for. 
OK regarding the OP, it happened by `0.12.16dev`, here is the diff:

https://github.com/bokeh/bokeh/compare/0.12.15...0.12.16dev1

This diff seems to fix (python 2 and python 3):
```
diff --git a/bokeh/io/export.py b/bokeh/io/export.py
index 147d87a66..265283c5f 100644
--- a/bokeh/io/export.py
+++ b/bokeh/io/export.py
@@ -171,8 +171,8 @@ def get_screenshot_as_png(obj, driver=None, **kwargs):

     with _tmp_html() as tmp:
         html = get_layout_html(obj, **kwargs)
-        with io.open(tmp.path, mode="wb") as file:
-            file.write(b(html))
+        with io.open(tmp.path, mode="w", encoding="utf-8") as file:
+            file.write(html)

         web_driver = driver if driver is not None else create_webdriver()
```
I'll make a minimal PR with this and a test to check export with inline resources, but we still need to run down that other potential issue. 

It's not actually inline vs cdn, it's minified vs non-minified
>It's not actually inline vs cdn, it's minified vs non-minified

Is that for the original bug or the other issue? I'll have a go at figuring out when that happened now.

Resolving patch:
diff --git a/bokeh/io/export.py b/bokeh/io/export.py
--- a/bokeh/io/export.py
+++ b/bokeh/io/export.py
@@ -36,6 +36,7 @@
 from ..embed import file_html
 from ..resources import INLINE
 from ..util.dependencies import import_required, detect_phantomjs
+from ..util.string import decode_utf8
 from .util import default_filename
 
 #-----------------------------------------------------------------------------
@@ -171,8 +172,8 @@ def get_screenshot_as_png(obj, driver=None, **kwargs):
 
     with _tmp_html() as tmp:
         html = get_layout_html(obj, **kwargs)
-        with io.open(tmp.path, mode="wb") as file:
-            file.write(b(html))
+        with io.open(tmp.path, mode="w", encoding="utf-8") as file:
+            file.write(decode_utf8(html))
 
         web_driver = driver if driver is not None else create_webdriver()
 


Test patch:
diff --git a/bokeh/io/tests/test_export.py b/bokeh/io/tests/test_export.py
--- a/bokeh/io/tests/test_export.py
+++ b/bokeh/io/tests/test_export.py
@@ -1,3 +1,4 @@
+# -*- coding: utf-8 -*-
 #-----------------------------------------------------------------------------
 # Copyright (c) 2012 - 2017, Anaconda, Inc. All rights reserved.
 #
@@ -27,7 +28,9 @@
 # Bokeh imports
 from bokeh.models.plots import Plot
 from bokeh.models.ranges import Range1d
+from bokeh.models.widgets.markups import Div
 from bokeh.io.export import create_webdriver, terminate_webdriver
+from bokeh.resources import Resources
 
 # Module under test
 import bokeh.io.export as bie
@@ -96,6 +99,32 @@ def test_get_screenshot_as_png_large_plot():
         # Have to manually clean up the driver session
         terminate_webdriver(driver)
 
+@pytest.mark.unit
+@pytest.mark.selenium
+def test_get_screenshot_as_png_with_unicode_minified():
+    layout = Div(text="유니 코드 지원을위한 작은 테스트")
+
+    driver = create_webdriver()
+    try:
+        png = bie.get_screenshot_as_png(layout, driver=driver, resources=Resources(mode="inline", minified=True))
+    finally:
+        # Have to manually clean up the driver session
+        terminate_webdriver(driver)
+    assert len(png.tobytes()) > 0
+
+@pytest.mark.unit
+@pytest.mark.selenium
+def test_get_screenshot_as_png_with_unicode_unminified():
+    layout = Div(text="유니 코드 지원을위한 작은 테스트")
+
+    driver = create_webdriver()
+    try:
+        png = bie.get_screenshot_as_png(layout, driver=driver, resources=Resources(mode="inline", minified=False))
+    finally:
+        # Have to manually clean up the driver session
+        terminate_webdriver(driver)
+    assert len(png.tobytes()) > 0
+
 @pytest.mark.unit
 @pytest.mark.selenium
 def test_get_svgs_no_svg_present():