A download button with custom CSS

[Update 2020-07-18] Revised the function to display a download button instead of a link after clicking a button, saving a UI step. The associated gist has been updated with the new version. Also, updated post title to reflect changes (hope that’s ok mods).

Hi, if it helps others, I created a first pass general file downloader for my personal Streamlit projects. I’ve also built an examples app to test it out with your files and to provide code snippets you can copy & paste.

See the following GitHub gist or code below.

I haven’t tested all files, and it’s an admittedly hacky solution I coded over the weekend. Improvements are welcome.

Run the example app with:

streamlit run https://gist.githubusercontent.com/chad-m/6be98ed6cf1c4f17d09b7f6e5ca2978f/raw/7ef3bf59741de2262a850020ab58e68947d3f052/streamlit_download_button.py

The download function code:


import base64
import os
import json
import pickle
import uuid
import re

import streamlit as st
import pandas as pd


def download_button(object_to_download, download_filename, button_text, pickle_it=False):
    """
    Generates a link to download the given object_to_download.

    Params:
    ------
    object_to_download:  The object to be downloaded.
    download_filename (str): filename and extension of file. e.g. mydata.csv,
    some_txt_output.txt download_link_text (str): Text to display for download
    link.
    button_text (str): Text to display on download button (e.g. 'click here to download file')
    pickle_it (bool): If True, pickle file.

    Returns:
    -------
    (str): the anchor tag to download object_to_download

    Examples:
    --------
    download_link(your_df, 'YOUR_DF.csv', 'Click to download data!')
    download_link(your_str, 'YOUR_STRING.txt', 'Click to download text!')

    """
    if pickle_it:
        try:
            object_to_download = pickle.dumps(object_to_download)
        except pickle.PicklingError as e:
            st.write(e)
            return None

    else:
        if isinstance(object_to_download, bytes):
            pass

        elif isinstance(object_to_download, pd.DataFrame):
            object_to_download = object_to_download.to_csv(index=False)

        # Try JSON encode for everything else
        else:
            object_to_download = json.dumps(object_to_download)

    try:
        # some strings <-> bytes conversions necessary here
        b64 = base64.b64encode(object_to_download.encode()).decode()

    except AttributeError as e:
        b64 = base64.b64encode(object_to_download).decode()

    button_uuid = str(uuid.uuid4()).replace('-', '')
    button_id = re.sub('\d+', '', button_uuid)

    custom_css = f""" 
        <style>
            #{button_id} {{
                background-color: rgb(255, 255, 255);
                color: rgb(38, 39, 48);
                padding: 0.25em 0.38em;
                position: relative;
                text-decoration: none;
                border-radius: 4px;
                border-width: 1px;
                border-style: solid;
                border-color: rgb(230, 234, 241);
                border-image: initial;

            }} 
            #{button_id}:hover {{
                border-color: rgb(246, 51, 102);
                color: rgb(246, 51, 102);
            }}
            #{button_id}:active {{
                box-shadow: none;
                background-color: rgb(246, 51, 102);
                color: white;
                }}
        </style> """

    dl_link = custom_css + f'<a download="{download_filename}" id="{button_id}" href="data:file/txt;base64,{b64}">{button_text}</a><br></br>'

    return dl_link

Gist:

11 Likes

Also, if anyone knows how to update the code to auto download the file instead of returning an anchor tag that has to then be clicked.

I was able to get the following download_link() return strings to download the file directly, but was not able to name the file with these:

return f'<iframe width="1" height="1" frameborder="0" src="data:file/txt;base64,{b64}"></iframe>'
return f'<meta name="TEST.txt" http-equiv="refresh" content="0; url=data:file/txt;base64,{b64}">'

[Update 2020-07-18] Revised the function to display a download button instead of a link after clicking a button, saving a UI step. The associated gist has been updated with the new version.

4 Likes

Great work Chad, thanks for sharing!

Out of curiosity, does it have to run with a specific runtime/32 bit version?

On my windows 10 64 bit laptop, I’ve got this error:

OSError: [WinError 193] %1 is not a valid Win32 application

If not, it is possible that my environments’ve recently been messed up on that laptop - I’m planning to reinstall them all next week. :slight_smile:

Thanks,
Charly

1 Like

Thank you for the feedback @Charly_Wargnier! I’m on a Mac and don’t have a windows machine to test it on. Do you have more info on the error - e.g. line number? I might be able to add an os check update.

PS are there Streamlit best coding practices for things like os checks?

1 Like

Sure, here’s the traceback:

WinDLL(os.path.abspath(filename))
  File "c:\program files\python38\lib\ctypes\__init__.py", line 369, in __init__
    self._handle = _dlopen(self._name, mode)
OSError: [WinError 193] %1 is not a valid Win32 application

Thanks,
Charly

1 Like

Great! Thank you @Charly_Wargnier . Is this happening when you click the download button or is it triggering an error when starting the example app?

1 Like

The latter, when trying to run streamlit run + py file.

1 Like

I looked into, but still not quite sure. Perhaps, like you said, it’s related to your environment. Keep me posted if you figure it out :slight_smile:

1 Like

Sure thing! :slight_smile:
Thanks Chad!

1 Like

@Chad_Mitchell How to add a download icon?

Hey @Chad_Mitchell, you should make a Streamlit component package out of it :slight_smile: (even if there’s no JS involved like streamlit-folium) to have a kind of definitive answer to this solution!

Then we can make PRs to add icons and stuff :wink:

Fanilo

1 Like

@Chad_Mitchell, fantastic answer, thank you so much! I had to adapt the CSS a bit to make it look like the new button design (guess streamlit changed it slightly since your original solution), maybe you can update this in your answer:

    #{button_id} {{
                display: inline-flex;
                align-items: center;
                justify-content: center;
                background-color: rgb(255, 255, 255);
                color: rgb(38, 39, 48);
                padding: .25rem .75rem;
                position: relative;
                text-decoration: none;
                border-radius: 4px;
                border-width: 1px;
                border-style: solid;
                border-color: rgb(230, 234, 241);
                border-image: initial;
            }} 
1 Like

Thank you; this is awesome! For some reason, the page refreshes when I type in the filename I would like and then click outside that text input box. It obviously works correctly on your demo page, so I’m a bit confused. Any ideas?

Hi @Chad_Mitchell

Thanks for the code above. It really helped. I am making a custom table component which already had a built in download button, so the auto download was just what I needed.

I was however having an issue of the filetype of the download. I was trying to download the file as excel and the browser didn’t interpret the base64 encoded string as excel.

Don’t know if it could help anyone but managed to find the right MIME type to make it work:

https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types

if isinstance(df,pd.DataFrame):

            towrite = io.BytesIO()

            df.to_excel(towrite)  # write to BytesIO buffer

            towrite.seek(0)  # reset pointer

            b64 = base64.b64encode(towrite.read()).decode()  

        return f'<iframe width="1" height="1" frameborder="0" src="data:application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;base64,{b64}"</iframe>'

Just a heads-up that might be interesting for some: With some little adjustment this download button can be used to download matplotlib figures as a .png file, too (see my use-case here: https://solar-mach.github.io)!

For that download_button() is defined as before in this thread. Then, the figure is created and shown in streamlit:

import io
import streamlit as st
import matplotlib.pyplot as plt

fig, ax = plt.subplots(figsize=(12, 8))
[...]
st.pyplot(fig)

Afterwards plt.savefig() is called (which unfortunately slows down the whole web-app by 1-2 seconds in loading) and handed over to download_button():

filename = 'some_name.png'
plot2 = io.BytesIO()
plt.savefig(plot2, format='png', bbox_inches="tight")
download_button_str = download_button(plot2.getvalue(), filename, f'Download figure as .png file', pickle_it=False)
st.markdown(download_button_str, unsafe_allow_html=True)

Would be really interested in feedback on this addition!

1 Like