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:

12 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}">'
1 Like

[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.

5 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

2 Likes

@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;
            }} 
2 Likes

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

This is great stuff Chad! Exactly the type of download button I was looking for :smiley:
Any way we can get the Streamlit dev team to implement this as a st.beta.file_downloader widget? Maybe open up a PR and see if they have some ideas about it :slight_smile:
I would guess itā€™s a common use case for a lot of people and right now itā€™s impossible to link a regular st.button to a file download.

Well, if you go take a look at the current draft PRs, you may have a surpriseā€¦but shhh I did not say anything :upside_down_face:

1 Like

:upside_down_face: Epic! :tada: :tada: :star_struck:
Why didnā€™t I check the latest PRs myself before commenting, haha thanks for the heads up Fanilo!
I feel like a kiddo waiting for his Christmas gift every new Streamlit release :stuck_out_tongue:

1 Like

for computer vision users,
modified from @Chad_Mitchell and add css from @jrieke

def download_button(object_to_download, download_filename, button_text, isPNG):
    """
    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(Pillow_image_from_cv_matrix, 'your_image.jpg', 'Click to me to download!')
    """

    buffered = BytesIO()
    if isPNG:
        object_to_download.save(buffered, format="PNG")
    else:
        object_to_download.save(buffered, format="JPEG")
    b64 = base64.b64encode(buffered.getvalue()).decode()

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

    custom_css = f""" 
        <style>
            #{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;
            }} 

            #{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

hope this helps! sample code streamlit for CV

1 Like