From de606408bba0acee40a61ed6b809237339f70cba Mon Sep 17 00:00:00 2001 From: vani s Date: Wed, 13 Dec 2023 09:14:54 +0530 Subject: [PATCH 1/8] add support for webp format --- examples/reference/panes/Image.ipynb | 5 ++-- panel/pane/image.py | 34 ++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/examples/reference/panes/Image.ipynb b/examples/reference/panes/Image.ipynb index e24615cc4e..45e53b50a2 100644 --- a/examples/reference/panes/Image.ipynb +++ b/examples/reference/panes/Image.ipynb @@ -47,7 +47,8 @@ "outputs": [], "source": [ "jpg_pane = pn.pane.Image('https://assets.holoviz.org/panel/samples/jpeg_sample.jpeg')\n", - "png_pane = pn.pane.Image('https://assets.holoviz.org/panel/samples/png_sample.png')" + "png_pane = pn.pane.Image('https://assets.holoviz.org/panel/samples/png_sample.png')\n", + "webp_pane = pn.pane.Image('https://mistral.ai/images/logo_hubc88c4ece131b91c7cb753f40e9e1cc5_2589_256x0_resize_q97_h2_lanczos_3.webp')" ] }, { @@ -58,7 +59,7 @@ }, "outputs": [], "source": [ - "pn.Column(jpg_pane, png_pane)" + "pn.Column(jpg_pane, png_pane, webp_pane)" ] }, { diff --git a/panel/pane/image.py b/panel/pane/image.py index 132a539d16..815ef7c657 100644 --- a/panel/pane/image.py +++ b/panel/pane/image.py @@ -496,3 +496,37 @@ def _transform_object(self, obj: Any) -> Dict[str, Any]: page = f'#page={self.start_page}' if getattr(self, 'start_page', None) else '' html = f'' return dict(text=escape(html)) + +class WEBP(ImageBase): + """ + The `WEBP` pane embeds a .webp image file in a panel if + provided a local path, or will link to a remote image if provided + a URL. + + Reference: https://developers.google.com/speed/webp/docs/riff_container + + :Example: + + >>> WEBP( + ... 'https://www.gstatic.com/webp/gallery/4.sm.webp', + ... alt_text='A nice tree', + ... link_url='https://en.wikipedia.org/wiki/WebP', + ... width=500, + ... caption='A nice tree' + ... ) + """ + + filetype: ClassVar[str] = 'webp' + + _extensions: ClassVar[Tuple[str, ...]] = ('webp',) + + @classmethod + def _imgshape(cls, data): + import struct + b = BytesIO(data) + b.read(12) # Skip RIFF header + if b.read(4) != b'VP8 ': # Check if VP8 chunk is present + raise ValueError("Invalid WebP file") + b.read(3) # Skip VP8 header + w, h = struct.unpack(" Date: Mon, 18 Dec 2023 09:52:45 +0530 Subject: [PATCH 2/8] rename WEBP class & add test --- examples/reference/panes/Image.ipynb | 2 +- panel/pane/__init__.py | 2 +- panel/pane/image.py | 8 ++++---- panel/tests/pane/test_image.py | 12 +++++++----- 4 files changed, 13 insertions(+), 11 deletions(-) diff --git a/examples/reference/panes/Image.ipynb b/examples/reference/panes/Image.ipynb index 45e53b50a2..24ad1e886c 100644 --- a/examples/reference/panes/Image.ipynb +++ b/examples/reference/panes/Image.ipynb @@ -48,7 +48,7 @@ "source": [ "jpg_pane = pn.pane.Image('https://assets.holoviz.org/panel/samples/jpeg_sample.jpeg')\n", "png_pane = pn.pane.Image('https://assets.holoviz.org/panel/samples/png_sample.png')\n", - "webp_pane = pn.pane.Image('https://mistral.ai/images/logo_hubc88c4ece131b91c7cb753f40e9e1cc5_2589_256x0_resize_q97_h2_lanczos_3.webp')" + "webp_pane = pn.pane.Image('https://assets.holoviz.org/panel/samples/webp_sample.webp')" ] }, { diff --git a/panel/pane/__init__.py b/panel/pane/__init__.py index 3e4fc20a3e..ddae867181 100644 --- a/panel/pane/__init__.py +++ b/panel/pane/__init__.py @@ -36,7 +36,7 @@ from .equation import LaTeX # noqa from .holoviews import HoloViews, Interactive # noqa from .image import ( # noqa - GIF, ICO, JPG, PDF, PNG, SVG, Image, + GIF, ICO, JPG, PDF, PNG, SVG, Image, WebP, ) from .ipywidget import IPyLeaflet, IPyWidget, Reacton # noqa from .markup import ( # noqa diff --git a/panel/pane/image.py b/panel/pane/image.py index 815ef7c657..4ac856d79e 100644 --- a/panel/pane/image.py +++ b/panel/pane/image.py @@ -497,9 +497,9 @@ def _transform_object(self, obj: Any) -> Dict[str, Any]: html = f'' return dict(text=escape(html)) -class WEBP(ImageBase): +class WebP(ImageBase): """ - The `WEBP` pane embeds a .webp image file in a panel if + The `WebP` pane embeds a .webp image file in a panel if provided a local path, or will link to a remote image if provided a URL. @@ -507,8 +507,8 @@ class WEBP(ImageBase): :Example: - >>> WEBP( - ... 'https://www.gstatic.com/webp/gallery/4.sm.webp', + >>> WebP( + ... 'https://assets.holoviz.org/panel/samples/webp_sample.webp', ... alt_text='A nice tree', ... link_url='https://en.wikipedia.org/wiki/WebP', ... width=500, diff --git a/panel/tests/pane/test_image.py b/panel/tests/pane/test_image.py index 8e649a52d5..d3e72da4ac 100644 --- a/panel/tests/pane/test_image.py +++ b/panel/tests/pane/test_image.py @@ -9,7 +9,7 @@ from requests.exceptions import MissingSchema from panel.pane import ( - GIF, ICO, JPG, PDF, PNG, SVG, + GIF, ICO, JPG, PDF, PNG, SVG, WebP, ) from panel.pane.markup import escape @@ -17,7 +17,7 @@ JPEG_FILE = 'https://assets.holoviz.org/panel/samples/jpeg_sample.jpeg' PNG_FILE = 'https://assets.holoviz.org/panel/samples/png_sample.png' SVG_FILE = 'https://assets.holoviz.org/panel/samples/svg_sample.svg' - +WEBP_FILE = 'https://assets.holoviz.org/panel/samples/webp_sample.webp' def test_jpeg_applies(): assert JPG.applies(JPEG_FILE) @@ -69,11 +69,13 @@ def test_svg_pane(document, comm): b'AAAAAAAAAAAAAAAAFBv/EABkRAAEFAAAAAAAAAAAAAAAAAAEAAjFxsf/aAAwDAQ' + \ b'ACEQMRAD8AA0qs5HvTHQcJdsChioXSbOr/2Q==', ico = b'AAABAAEAAgEAAAEAIAA0AAAAFgAAACgAAAACAAAAAgAAAAEAIAAAAAAACAAAAHQ' + \ - b'SAAB0EgAAAAAAAAAAAAD//////////wAAAAA=') - + b'SAAB0EgAAAAAAAAAAAAD//////////wAAAAA=', + webp= b'iVBORw0KGgoAAAANSUhEUgAAAAIAAAABCAYAAAD0In+KAAAAFElEQVQIHQEJAPbA' + \ + b'WNYYP/h4uMAFL0EwlEn99gAAAAASUVORK5CYIA==' + ) def test_imgshape(): - for t in [PNG, JPG, GIF, ICO]: + for t in [PNG, JPG, GIF, ICO, WebP, ]: w,h = t._imgshape(b64decode(twopixel[t.name.lower()])) assert w == 2 assert h == 1 From 58bdecec566497508c742683cbcd89810c8b7a73 Mon Sep 17 00:00:00 2001 From: vani s Date: Mon, 18 Dec 2023 21:17:29 +0530 Subject: [PATCH 3/8] update test function for image --- panel/tests/pane/test_image.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/panel/tests/pane/test_image.py b/panel/tests/pane/test_image.py index d3e72da4ac..f3241f22df 100644 --- a/panel/tests/pane/test_image.py +++ b/panel/tests/pane/test_image.py @@ -74,11 +74,12 @@ def test_svg_pane(document, comm): b'WNYYP/h4uMAFL0EwlEn99gAAAAASUVORK5CYIA==' ) -def test_imgshape(): - for t in [PNG, JPG, GIF, ICO, WebP, ]: - w,h = t._imgshape(b64decode(twopixel[t.name.lower()])) - assert w == 2 - assert h == 1 + +@pytest.mark.parametrize('t', [PNG, JPG, GIF, ICO, WebP], ids=lambda t: t.name.lower()) +def test_imgshape(t): + w, h = t._imgshape(b64decode(twopixel[t.name.lower()])) + assert w == 2 + assert h == 1 def test_load_from_byteio(): """Testing a loading a image from a ByteIo""" From 4b375eea694bb132a741aaea3274eb02ca320236 Mon Sep 17 00:00:00 2001 From: vani s Date: Mon, 18 Dec 2023 22:32:33 +0530 Subject: [PATCH 4/8] update webp image bytes --- panel/tests/pane/test_image.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/panel/tests/pane/test_image.py b/panel/tests/pane/test_image.py index f3241f22df..c246cae56f 100644 --- a/panel/tests/pane/test_image.py +++ b/panel/tests/pane/test_image.py @@ -70,8 +70,8 @@ def test_svg_pane(document, comm): b'ACEQMRAD8AA0qs5HvTHQcJdsChioXSbOr/2Q==', ico = b'AAABAAEAAgEAAAEAIAA0AAAAFgAAACgAAAACAAAAAgAAAAEAIAAAAAAACAAAAHQ' + \ b'SAAB0EgAAAAAAAAAAAAD//////////wAAAAA=', - webp= b'iVBORw0KGgoAAAANSUhEUgAAAAIAAAABCAYAAAD0In+KAAAAFElEQVQIHQEJAPbA' + \ - b'WNYYP/h4uMAFL0EwlEn99gAAAAASUVORK5CYIA==' + webp= b'UklGRi4AAABXRUJQVlA4ICIAAACQAQCdASoCAAEAAgA0JZwAAudZuFQA/vXf1fmh' + \ + b'/Al4AAAA' ) From ea097fbcbecf8123d887fe3190fd7d2bde440bb7 Mon Sep 17 00:00:00 2001 From: vani s Date: Thu, 21 Dec 2023 19:45:51 +0530 Subject: [PATCH 5/8] cover all webp formats --- panel/pane/image.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/panel/pane/image.py b/panel/pane/image.py index 4ac856d79e..33bec18335 100644 --- a/panel/pane/image.py +++ b/panel/pane/image.py @@ -525,8 +525,19 @@ def _imgshape(cls, data): import struct b = BytesIO(data) b.read(12) # Skip RIFF header - if b.read(4) != b'VP8 ': # Check if VP8 chunk is present + chunk_header = b.read(4) + if chunk_header == b'VP8 ': # Lossy WebP + b.read(3) # Skip VP8 header + w, h = struct.unpack("> 14) & 0x3FFF) + 1 # Height is stored in the next 14 bits + elif chunk_header == b'VP8X': # Extended WebP + b.read(4) # Skip VP8X header + w = struct.unpack(" Date: Sat, 17 Feb 2024 12:50:00 +0100 Subject: [PATCH 6/8] Fix webp header unpacking --- panel/pane/image.py | 26 ++++++++------------------ panel/tests/pane/test_image.py | 4 ++-- 2 files changed, 10 insertions(+), 20 deletions(-) diff --git a/panel/pane/image.py b/panel/pane/image.py index 33bec18335..d9b00c49d7 100644 --- a/panel/pane/image.py +++ b/panel/pane/image.py @@ -522,22 +522,12 @@ class WebP(ImageBase): @classmethod def _imgshape(cls, data): - import struct - b = BytesIO(data) - b.read(12) # Skip RIFF header - chunk_header = b.read(4) - if chunk_header == b'VP8 ': # Lossy WebP - b.read(3) # Skip VP8 header - w, h = struct.unpack("> 14) & 0x3FFF) + 1 # Height is stored in the next 14 bits - elif chunk_header == b'VP8X': # Extended WebP - b.read(4) # Skip VP8X header - w = struct.unpack("> 14) & 0x3FFF) + 1 + elif wptype == ' ': + b.read(6) + w = int.from_bytes(b.read(2), 'little') + 1 + h = int.from_bytes(b.read(2), 'little') + 1 return int(w), int(h) From f8e17bbdb67567fa821e90e9d6d7b7e920d68c5b Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Sun, 18 Feb 2024 12:24:40 +0100 Subject: [PATCH 8/8] Add WebP reference notebook --- examples/reference/panes/WebP.ipynb | 102 ++++++++++++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 examples/reference/panes/WebP.ipynb diff --git a/examples/reference/panes/WebP.ipynb b/examples/reference/panes/WebP.ipynb new file mode 100644 index 0000000000..c511ae8e07 --- /dev/null +++ b/examples/reference/panes/WebP.ipynb @@ -0,0 +1,102 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import panel as pn\n", + "\n", + "pn.extension()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The `WebP` pane embeds a `.web` image file in a panel if provided a local path, or will link to a remote image if provided a URL.\n", + "\n", + "#### Parameters:\n", + "\n", + "For details on other options for customizing the component see the [layout](../../how_to/layout/index.md) and [styling](../../how_to/styling/index.md) how-to guides.\n", + "\n", + "* **``alt_text``** (str, default=None): alt text to add to the image tag. The alt text is shown when a user cannot load or display the image. \n", + "* **``embed``** (boolean, default=False): If given a URL to an image this determines whether the image will be embedded as base64 or merely linked to.\n", + "* **``fixed_aspect``** (boolean, default=True): Whether the aspect ratio of the image should be forced to be equal.\n", + "* **``link_url``** (str, default=None): A link URL to make the image clickable and link to some other website.\n", + "* **``object``** (str or object): The PNG file to display. Can be a string pointing to a local or remote file, or an object with a ``_repr_png_`` method.\n", + "* **``style``** (dict): Dictionary specifying CSS styles\n", + "\n", + "___" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The `WebP` pane can be pointed at any local or remote `.webp` 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": [ + "webp_pane = pn.pane.WebP('https://assets.holoviz.org/panel/samples/webp_sample.webp')\n", + "\n", + "webp_pane" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can scale the size of the image by setting a specific fixed `width` or `height`:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "webp_pane.clone(width=400)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Alternatively we can scale the width and height using the `sizing_mode`:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "pn.pane.WebP(\n", + " 'https://assets.holoviz.org/panel/samples/webp_sample2.webp', sizing_mode='scale_width'\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Note that by default the aspect ratio of the image is fixed, and so there may be a gap beside or below the image even in responsive sizing modes. To override this behavior set `fixed_aspect=False` or provide fixed `width` and `height` values." + ] + } + ], + "metadata": { + "language_info": { + "name": "python", + "pygments_lexer": "ipython3" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +}