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

Add PDF pane #2444

Merged
merged 3 commits into from
Jun 28, 2021
Merged
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
74 changes: 74 additions & 0 deletions examples/reference/panes/PDF.ipynb
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
{
"cells": [
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"import panel as pn\n",
"\n",
"pn.extension()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"The ``PDF`` pane embeds an ``.pdf`` document in a panel if provided a local path, or will link to a remote file if provided a URL.\n",
"\n",
"#### Parameters:\n",
"\n",
"For layout and styling related parameters see the [customization user guide](../../user_guide/Customization.ipynb).\n",
"\n",
"* **``embed``** (boolean, default=False): If given a URL to a file this determines whether the image will be embedded as base64 or merely linked to.\n",
"* **``object``** (str or object): The PDF file to display. Can be a string pointing to a local or remote file, or an object with a ``_repr_pdf_`` method.\n",
"* **``style``** (dict): Dictionary specifying CSS styles\n",
"\n",
"___"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"The ``PDF`` pane can be pointed at any local or remote ``.pdf`` file. If given a URL starting with ``http`` or ``https``, the ``embed`` parameter determines whether the image will be embedded or linked to:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"pdf_pane = pn.pane.PDF('https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf', width=700, height=1000)\n",
"\n",
"pdf_pane"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Like any other pane, the ``PDF`` pane can be updated by setting the ``object`` parameter:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"pdf_pane.object = 'http://www.africau.edu/images/default/sample.pdf'"
]
}
],
"metadata": {
"language_info": {
"name": "python",
"pygments_lexer": "ipython3"
}
},
"nbformat": 4,
"nbformat_minor": 4
}
2 changes: 1 addition & 1 deletion panel/pane/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
from .holoviews import HoloViews, Interactive # noqa
from .idom import IDOM # noqa0
from .ipywidget import IPyWidget # noqa
from .image import GIF, JPG, PNG, SVG # noqa
from .image import GIF, JPG, PDF, PNG, SVG # noqa
from .markup import DataFrame, HTML, JSON, Markdown, Str # noqa
from .media import Audio, Video # noqa
from .perspective import Perspective # noqa
Expand Down
132 changes: 80 additions & 52 deletions panel/pane/image.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,64 +13,40 @@
from ..util import isfile, isurl


class ImageBase(DivPaneBase):
"""
Encodes an image as base64 and wraps it in a Bokeh Div model.
This is an abstract base class that needs the image type
to be specified and specific code for determining the image shape.

The imgtype determines the filetype, extension, and MIME type for
this image. Each image type (png,jpg,gif) has a base class that
supports anything with a `_repr_X_` method (where X is `png`,
`gif`, etc.), a local file with the given file extension, or a
HTTP(S) url with the given extension. Subclasses of each type can
provide their own way of obtaining or generating a PNG.
"""

alt_text = param.String(default=None, doc="""
alt text to add to the image tag. The alt text is shown when a
user cannot load or display the image.""")

link_url = param.String(default=None, doc="""
A link URL to make the image clickable and link to some other
website.""")
class FileBase(DivPaneBase):

embed = param.Boolean(default=True, doc="""
Whether to embed the image as base64.""")

imgtype = 'None'
Whether to embed the file as base64.""")

_rerender_params = ['alt_text', 'link_url', 'embed', 'object', 'style', 'width', 'height']

_target_transforms = {'object': """'<img src="' + value + '"></img>'"""}
_rerender_params = ['embed', 'object', 'style', 'width', 'height']

__abstract = True

def _type_error(self, object):
if isinstance(object, string_types):
raise ValueError("%s pane cannot parse string that is not a filename "
"or URL." % type(self).__name__)
super()._type_error(object)

@classmethod
def applies(cls, obj):
imgtype = cls.imgtype
if hasattr(obj, '_repr_{}_'.format(imgtype)):
filetype = cls.filetype
if hasattr(obj, '_repr_{}_'.format(filetype)):
return True
if isinstance(obj, string_types):
if isfile(obj) and obj.endswith('.'+imgtype):
if isfile(obj) and obj.endswith('.'+filetype):
return True
if isurl(obj, [cls.imgtype]):
if isurl(obj, [cls.filetype]):
return True
elif isurl(obj, None):
return 0
if hasattr(obj, 'read'): # Check for file like object
return True
return False

def _type_error(self, object):
if isinstance(object, string_types):
raise ValueError("%s pane cannot parse string that is not a filename "
"or URL." % type(self).__name__)
super()._type_error(object)

def _img(self):
if hasattr(self.object, '_repr_{}_'.format(self.imgtype)):
return getattr(self.object, '_repr_' + self.imgtype + '_')()
def _data(self):
if hasattr(self.object, '_repr_{}_'.format(self.filetype)):
return getattr(self.object, '_repr_' + self.filetype + '_')()
if isinstance(self.object, string_types):
if isfile(self.object):
with open(self.object, 'rb') as f:
Expand All @@ -84,12 +60,43 @@ def _img(self):
r = requests.request(url=self.object, method='GET')
return r.content


class ImageBase(FileBase):
"""
Encodes an image as base64 and wraps it in a Bokeh Div model.
This is an abstract base class that needs the image type
to be specified and specific code for determining the image shape.

The filetype determines the filetype, extension, and MIME type for
this image. Each image type (png,jpg,gif) has a base class that
supports anything with a `_repr_X_` method (where X is `png`,
`gif`, etc.), a local file with the given file extension, or a
HTTP(S) url with the given extension. Subclasses of each type can
provide their own way of obtaining or generating a PNG.
"""

alt_text = param.String(default=None, doc="""
alt text to add to the image tag. The alt text is shown when a
user cannot load or display the image.""")

link_url = param.String(default=None, doc="""
A link URL to make the image clickable and link to some other
website.""")

filetype = 'None'

_rerender_params = ['alt_text', 'link_url', 'embed', 'object', 'style', 'width', 'height']

_target_transforms = {'object': """'<img src="' + value + '"></img>'"""}

__abstract = True

def _b64(self):
data = self._img()
data = self._data()
if not isinstance(data, bytes):
data = data.encode('utf-8')
b64 = base64.b64encode(data).decode("utf-8")
return "data:image/"+self.imgtype+f";base64,{b64}"
return "data:image/"+self.filetype+f";base64,{b64}"

def _imgshape(self, data):
"""Calculate and return image width,height"""
Expand All @@ -99,7 +106,7 @@ def _get_properties(self):
p = super()._get_properties()
if self.object is None:
return dict(p, text='<img></img>')
data = self._img()
data = self._data()
if not isinstance(data, bytes):
data = base64.b64decode(data)
width, height = self._imgshape(data)
Expand All @@ -116,7 +123,7 @@ def _get_properties(self):
src = self.object
else:
b64 = base64.b64encode(data).decode("utf-8")
src = "data:image/"+self.imgtype+";base64,{b64}".format(b64=b64)
src = "data:image/"+self.filetype+";base64,{b64}".format(b64=b64)

smode = self.sizing_mode
if smode in ['fixed', None]:
Expand Down Expand Up @@ -144,7 +151,7 @@ def _get_properties(self):

class PNG(ImageBase):

imgtype = 'png'
filetype = 'png'

@classmethod
def _imgshape(cls, data):
Expand All @@ -155,7 +162,7 @@ def _imgshape(cls, data):

class GIF(ImageBase):

imgtype = 'gif'
filetype = 'gif'

@classmethod
def _imgshape(cls, data):
Expand All @@ -166,7 +173,7 @@ def _imgshape(cls, data):

class JPG(ImageBase):

imgtype = 'jpg'
filetype = 'jpg'

@classmethod
def _imgshape(cls, data):
Expand All @@ -193,7 +200,7 @@ class SVG(ImageBase):
Whether to enable base64 encoding of the SVG, base64 encoded
SVGs do not support links.""")

imgtype = 'svg'
filetype = 'svg'

_rerender_params = ImageBase._rerender_params + ['encode']

Expand All @@ -208,14 +215,14 @@ def _type_error(self, object):
"URL or a SVG XML contents." % type(self).__name__)
super()._type_error(object)

def _img(self):
def _data(self):
if (isinstance(self.object, string_types) and
self.object.lstrip().startswith('<svg')):
return self.object
return super()._img()
return super()._data()

def _b64(self):
data = self._img()
data = self._data()
if not isinstance(data, bytes):
data = data.encode('utf-8')
b64 = base64.b64encode(data).decode("utf-8")
Expand All @@ -228,7 +235,7 @@ def _get_properties(self):
p = super(ImageBase, self)._get_properties()
if self.object is None:
return dict(p, text='<img></img>')
data = self._img()
data = self._data()
width, height = self._imgshape(data)
if not isinstance(data, bytes):
data = data.encode('utf-8')
Expand All @@ -242,3 +249,24 @@ def _get_properties(self):
else:
html = data.decode("utf-8")
return dict(p, width=width, height=height, text=escape(html))


class PDF(FileBase):

filetype = 'pdf'

def _get_properties(self):
p = super()._get_properties()
if self.object is None:
return dict(p, text='<embed></embed>')
if self.embed:
data = self._data()
if not isinstance(data, bytes):
data = data.encode('utf-8')
base64_pdf = base64.b64encode(data).decode("utf-8")
src = f"data:application/pdf;base64,{base64_pdf}"
else:
src = self.object
w, h = self.width or '100%', self.height or '100%'
html = f'<embed src="{src}" width={w!r} height={h!r} type="application/pdf">'
return dict(p, text=escape(html))
2 changes: 1 addition & 1 deletion panel/pane/plot.py
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,7 @@ def _imgshape(self, data):
w, h = self.object.get_size_inches()
return int(w*72), int(h*72)

def _img(self):
def _data(self):
self.object.set_dpi(self.dpi)
b = BytesIO()

Expand Down
23 changes: 19 additions & 4 deletions panel/tests/pane/test_image.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

from base64 import b64decode, b64encode

from panel.pane import GIF, JPG, PNG, SVG
from panel.pane import GIF, JPG, PDF, PNG, SVG
from panel.pane.markup import escape
from io import BytesIO, StringIO

Expand Down Expand Up @@ -70,7 +70,7 @@ def test_load_from_byteio():
memory.write(image_file.read())

image_pane = PNG(memory)
image_data = image_pane._img()
image_data = image_pane._data()
assert b'PNG' in image_data

@pytest.mark.skipif(sys.version_info.major <= 2, reason="Doesn't work with python 2")
Expand All @@ -83,7 +83,7 @@ def test_load_from_stringio():
memory.write(str(image_file.read()))

image_pane = PNG(memory)
image_data = image_pane._img()
image_data = image_pane._data()
assert 'PNG' in image_data

def test_loading_a_image_from_url():
Expand All @@ -92,7 +92,7 @@ def test_loading_a_image_from_url():
'1700_CE_world_map.PNG'

image_pane = PNG(url)
image_data = image_pane._img()
image_data = image_pane._data()
assert b'PNG' in image_data


Expand All @@ -116,3 +116,18 @@ def test_image_link_url(document, comm):
model = image_pane.get_root(document, comm)

assert model.text.startswith('&lt;a href=&quot;http://anaconda.org&quot;')


def test_pdf_embed(document, comm):
pdf_pane = PDF('https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf')
model = pdf_pane.get_root(document, comm)

assert model.text.startswith("&lt;embed src=&quot;data:application/pdf;base64,")


def test_pdf_no_embed(document, comm):
url = 'https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf'
pdf_pane = PDF(url, embed=False)
model = pdf_pane.get_root(document, comm)

assert model.text.startswith(f"&lt;embed src=&quot;{url}")