Repository: holoviz/panel

Issue:
Enable saving to BytesIO png.
I've often wanted to make it possible for users to download a `.png` of a specific panel object. And it is a painful process as the `.save` method only supports saving to `.png` if the filename provided is a string. But for filedownload you need to save to a `BytesIO`, i.e. you would be providing a `BytesIO` as `filename`.

Here is a user also having the problem https://discourse.holoviz.org/t/how-to-save-panel-object-to-png-without-file-extension-when-using-bytesio/3152

## Solution

Expose the `as_png` variable in the `.save` method to the user.

## Workaround

The example on discourse also contains a workaround using a temporary file.


Discussion:

Resolving patch:
diff --git a/panel/io/save.py b/panel/io/save.py
--- a/panel/io/save.py
+++ b/panel/io/save.py
@@ -98,7 +98,7 @@ def get_layout_html(obj, resources, width, height, **kwargs):
         if img.width == 0 or img.height == 0:
             raise ValueError("unable to save an empty image")
 
-        img.save(filename)
+        img.save(filename, format="png")
     except Exception:
         raise
     finally:
@@ -148,7 +148,8 @@ def file_html(models, resources, title=None, template=BASE_TEMPLATE,
 def save(panel, filename, title=None, resources=None, template=None,
          template_variables=None, embed=False, max_states=1000,
          max_opts=3, embed_json=False, json_prefix='', save_path='./',
-         load_path=None, progress=True, embed_states={}, **kwargs):
+         load_path=None, progress=True, embed_states={}, as_png=None,
+         **kwargs):
     """
     Saves Panel objects to file.
 
@@ -184,6 +185,9 @@ def save(panel, filename, title=None, resources=None, template=None,
       Whether to report progress
     embed_states: dict (default={})
       A dictionary specifying the widget values to embed for each widget
+    save_png: boolean (default=None)
+        To save as a .png. If None save_png will be true if filename is
+        string and ends with png.
     """
     from ..pane import PaneBase
     from ..template import BaseTemplate
@@ -191,7 +195,8 @@ def save(panel, filename, title=None, resources=None, template=None,
     if isinstance(panel, PaneBase) and len(panel.layout) > 1:
         panel = panel.layout
 
-    as_png = isinstance(filename, str) and filename.endswith('png')
+    if as_png is None:
+        as_png = isinstance(filename, str) and filename.endswith('png')
 
     if isinstance(panel, Document):
         doc = panel
diff --git a/panel/viewable.py b/panel/viewable.py
--- a/panel/viewable.py
+++ b/panel/viewable.py
@@ -733,7 +733,8 @@ def embed(self, max_states=1000, max_opts=3, json=False, json_prefix='',
     def save(self, filename, title=None, resources=None, template=None,
              template_variables=None, embed=False, max_states=1000,
              max_opts=3, embed_json=False, json_prefix='', save_path='./',
-             load_path=None, progress=True, embed_states={}, **kwargs):
+             load_path=None, progress=True, embed_states={}, as_png=None,
+             **kwargs):
         """
         Saves Panel objects to file.
 
@@ -767,11 +768,14 @@ def save(self, filename, title=None, resources=None, template=None,
           Whether to report progress
         embed_states: dict (default={})
           A dictionary specifying the widget values to embed for each widget
+        save_png: boolean (default=None)
+          To save as a .png. If None save_png will be true if filename is
+          string and ends with png.
         """
         return save(self, filename, title, resources, template,
                     template_variables, embed, max_states, max_opts,
                     embed_json, json_prefix, save_path, load_path,
-                    progress, embed_states, **kwargs)
+                    progress, embed_states, as_png, **kwargs)
 
     def server_doc(self, doc=None, title=None, location=True):
         """


Test patch: