Repository: holoviz/panel

Issue:
PDF Pane not always rendering the document
Local large PDF files cannot all be rendered, and even files of moderate size. The implementation converts the PDF to a base64-string, and uses the HTML `embed` tag whose `src` is assigned a data URL built from the base64-string. When that string is over a limit, that depends on the browser in use, the PDF pane isn't rendered at all and there's no error logged.

I'm sharing this 2Mb PDF file that cannot be rendered on Chrome when served locally but can be rendered on Firefox.

[pdfsample50.pdf](https://github.com/holoviz/panel/files/9115028/pdfsample50.pdf)

There are two posts on Discourse that seem related to this issue:
* https://discourse.holoviz.org/t/pdf-pane-pn-pane-pdf/3640/14
* https://discourse.holoviz.org/t/issue-on-displaying-pdfs/3884



Discussion:

Resolving patch:
diff --git a/panel/models/__init__.py b/panel/models/__init__.py
--- a/panel/models/__init__.py
+++ b/panel/models/__init__.py
@@ -10,7 +10,7 @@
 from .ipywidget import IPyWidget  # noqa
 from .layout import Card  # noqa
 from .location import Location  # noqa
-from .markup import HTML, JSON  # noqa
+from .markup import HTML, JSON, PDF  # noqa
 from .reactive_html import ReactiveHTML  # noqa
 from .state import State  # noqa
 from .tabs import Tabs  # noqa
diff --git a/panel/models/markup.py b/panel/models/markup.py
--- a/panel/models/markup.py
+++ b/panel/models/markup.py
@@ -25,3 +25,8 @@ class JSON(Markup):
     hover_preview = Bool(default=False, help="Whether to show a hover preview for collapsed nodes.")
 
     theme = String(default='dark', help="Whether to expand all JSON nodes.")
+
+
+class PDF(Div):
+    embed = Bool(True, help="Whether to embed the file")
+    start_page = Int(default=1, help="Start page of the pdf, by default the first page.")
diff --git a/panel/pane/image.py b/panel/pane/image.py
--- a/panel/pane/image.py
+++ b/panel/pane/image.py
@@ -9,14 +9,17 @@
 from io import BytesIO
 from pathlib import PurePath
 from typing import (
-    Any, ClassVar, List, Mapping,
+    TYPE_CHECKING, Any, ClassVar, List, Mapping,
 )
 
 import param
 
+from ..models import PDF as _BkPDF
 from ..util import isfile, isurl
 from .markup import DivPaneBase, escape
 
+if TYPE_CHECKING:
+    from bokeh.model import Model
 
 class FileBase(DivPaneBase):
 
@@ -378,20 +381,33 @@ class PDF(FileBase):
     ... )
     """
 
+    start_page = param.Integer(1, doc="""
+        Start page of the pdf, by default the first page.""")
+
     filetype: ClassVar[str] = 'pdf'
 
+    _bokeh_model: ClassVar[Model] = _BkPDF
+
+    _rerender_params: ClassVar[List[str]] = FileBase._rerender_params + ['start_page']
+
     def _get_properties(self):
         p = super()._get_properties()
+        p["embed"] = self.embed
+        p["start_page"] = self.start_page
+        p["width"] = self.width
+        p["height"] = self.height
+
         if self.object is None:
             return dict(p, text='<embed></embed>')
         if self.embed:
+            # This is handled by the Typescript Bokeh model to be able to render large PDF files (>2MB).
             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}"
+            return dict(p, text=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">'
+        html = f'<embed src="{src}#page={self.start_page}" width={w!r} height={h!r} type="application/pdf">'
         return dict(p, text=escape(html))


Test patch:
diff --git a/panel/tests/pane/test_image.py b/panel/tests/pane/test_image.py
--- a/panel/tests/pane/test_image.py
+++ b/panel/tests/pane/test_image.py
@@ -134,13 +134,6 @@ def test_image_link_url(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)
diff --git a/panel/tests/ui/widgets/test_image.py b/panel/tests/ui/widgets/test_image.py
new file mode 100644
--- /dev/null
+++ b/panel/tests/ui/widgets/test_image.py
@@ -0,0 +1,53 @@
+import time
+
+import pytest
+
+pytestmark = pytest.mark.ui
+
+from panel.io.server import serve
+from panel.pane import PDF
+
+
+def test_pdf_embed(page, port):
+    url = 'https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf'
+    pdf_pane = PDF(url)
+
+    serve(pdf_pane, port=port, threaded=True, show=False)
+    time.sleep(0.2)
+
+    page.goto(f"http://localhost:{port}")
+
+    src = page.locator("embed").get_attribute('src')
+
+    assert src.startswith(f"blob:http://localhost:{port}")
+    assert src.endswith("#page=1")
+
+
+def test_pdf_embed_start_page(page, port):
+    # The pdf does not have 2 pages just to verify #page is set
+    url = 'https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf'
+    pdf_pane = PDF(url, start_page=22)
+
+    serve(pdf_pane, port=port, threaded=True, show=False)
+    time.sleep(0.2)
+
+    page.goto(f"http://localhost:{port}")
+
+    src = page.locator("embed").get_attribute('src')
+
+    assert src.startswith(f"blob:http://localhost:{port}")
+    assert src.endswith("#page=22")
+
+
+def test_pdf_no_embed_start_page(page, port):
+    # The pdf does not have 2 pages just to verify #page is set
+    url = 'https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf'
+    pdf_pane = PDF(url, start_page=22, embed=False)
+
+    serve(pdf_pane, port=port, threaded=True, show=False)
+    time.sleep(0.2)
+
+    page.goto(f"http://localhost:{port}")
+
+    src = page.locator("embed").get_attribute('src')
+    assert src == url + "#page=22"