Skip to content

Commit

Permalink
Add PDF pane (#2444)
Browse files Browse the repository at this point in the history
* Add PDF pane

* Add tests

* Fix matplotlib pane
  • Loading branch information
philippjfr committed Jun 28, 2021
1 parent 7681627 commit 526f70f
Show file tree
Hide file tree
Showing 5 changed files with 175 additions and 58 deletions.
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}")

0 comments on commit 526f70f

Please sign in to comment.