eligrey / FileSaver.js

An HTML5 saveAs() FileSaver implementation
https://eligrey.com/blog/saving-generated-files-on-the-client-side/
Other
21.59k stars 4.38k forks source link

Downloading jpeg on Chrome iOS only results in 1) opening image in new tab or 2) downloading file named "document" with no extension #686

Open jparismorgan opened 3 years ago

jparismorgan commented 3 years ago

Hello, I am having trouble downloading an image on Chrome iOS 87.0.4280.77. I am using an iPhone 12 running iOS 14.3-beta. I am taking a screenshot with MediaRecorder and then creating a blob with const currentBlob = new Blob([buffer], {type: 'image/jpeg'}). My goal is to be able to trigger a download of the image, with the name and file extension set.

Issue: Testing with FileSaver.js with code from v2.0.4 or master

When I call saveAs(currentBlob, "name.jpg"), the file pops up in a new tab. I can long press on it and get the ability to save to photos (and other things), but I want a direct download.

For both versions of FileSaver.js I have tried changing the type of the blob (b/c I have read that may force Chrome to automatically download the file), but that results in no tab being opened and no download pop-up:

const currentBlob = new Blob([buffer], {type: 'image/jpeg'})
const rawBlob = new Blob([currentBlob])
const octetBlob = new Blob([currentBlob], {type: 'application/octet-stream'})
saveAs(blob / rawBlob / octetBlob, "name.jpg")

For contrast, Safari iOS shows the system dialog for a download in all three of these cases, with both versions of FileSaver.js.

Solution Attempt 1: Using a to download the image

The basic way to download a file is below. This results in the image being opened in a new window with no download prompt:

const currentBlob = new Blob([buffer], {type: 'image/jpeg'})
const a = document.createElement('a')
a.download = currentName
a.href = URL.createObjectURL(currentBlob)
a.rel = 'noopener'
document.body.appendChild(a)
click(a)
document.body.removeChild(a)

If I instead encode the Blob into a Base64 encoded string with readAsDataURL, I can get the file to download, but it is named 'document' and has no file extension (though if I rename the file to "name.jpg" then I can see it is in fact an image):

const currentBlob = new Blob([buffer], {type: 'image/jpeg'})
const reader = new FileReader()
reader.onloadend = () => {
    const a = document.createElement('a')
    a.download = currentName
    a.href = reader.result // I.e. data:image/jpeg;base64,/9j/4AA...
    a.rel = 'noopener'
    document.body.appendChild(a)
    click(a)
    document.body.removeChild(a)
}
reader.readAsDataURL(currentBlob)
alt text alt text

I have also tried changing the Base64 encoded string in several ways to try to rename the file, but to no effect.

reader.onloadend = () => {
    ...
    const url = reader.result
    const urlWithHeaders = url.replace(';', `;name=${currentFilename};headers=Content-Disposition%3A%20attachment%3B%20filename=%22${currentFilename}%22;`)
    const urlFile = url.replace(/^data:[^;]*;/, 'data:attachment/file;')
    const urlOctet = url.replace('image/jpeg', 'binary/octet-stream')
    a.href = url / urlWithHeaders / urlFile / urlOctet
    ...
}

Solution Attempt 2: Using a new window

I have also tried by opening a new window, but it just opens the image in a new window and does not prompt a download:

const currentBlob = new Blob([buffer], {type: 'image/jpeg'})
const newWindow = window.open(URL.createObjectURL(currentBlob), '_blank')

Questions

There are several issues that seem potentially related, but all of them are quite old and haven't focused on images specifically: https://github.com/eligrey/FileSaver.js/issues/506, https://github.com/eligrey/FileSaver.js/issues/576, https://github.com/eligrey/FileSaver.js/issues/343

I have also tried with an mp4 but, just like these questions describe, it is not working:

mauriblint commented 3 years ago

Having the same issue here with Chrome for iOS, any workaround?

xiaoluntian commented 3 years ago

Hello, I am having trouble downloading an image on Chrome iOS 87.0.4280.77. I am using an iPhone 12 running iOS 14.3-beta. I am taking a screenshot with MediaRecorder and then creating a blob with const currentBlob = new Blob([buffer], {type: 'image/jpeg'}). My goal is to be able to trigger a download of the image, with the name and file extension set.

Issue: Testing with FileSaver.js with code from v2.0.4 or master

When I call saveAs(currentBlob, "name.jpg"), the file pops up in a new tab. I can long press on it and get the ability to save to photos (and other things), but I want a direct download.

For both versions of FileSaver.js I have tried changing the type of the blob (b/c I have read that may force Chrome to automatically download the file), but that results in no tab being opened and no download pop-up:

const currentBlob = new Blob([buffer], {type: 'image/jpeg'})
const rawBlob = new Blob([currentBlob])
const octetBlob = new Blob([currentBlob], {type: 'application/octet-stream'})
saveAs(blob / rawBlob / octetBlob, "name.jpg")

For contrast, Safari iOS shows the system dialog for a download in all three of these cases, with both versions of FileSaver.js.

Solution Attempt 1: Using a to download the image

The basic way to download a file is below. This results in the image being opened in a new window with no download prompt:

const currentBlob = new Blob([buffer], {type: 'image/jpeg'})
const a = document.createElement('a')
a.download = currentName
a.href = URL.createObjectURL(currentBlob)
a.rel = 'noopener'
document.body.appendChild(a)
click(a)
document.body.removeChild(a)

If I instead encode the Blob into a Base64 encoded string with readAsDataURL, I can get the file to download, but it is named 'document' and has no file extension (though if I rename the file to "name.jpg" then I can see it is in fact an image):

const currentBlob = new Blob([buffer], {type: 'image/jpeg'})
const reader = new FileReader()
reader.onloadend = () => {
    const a = document.createElement('a')
    a.download = currentName
    a.href = reader.result // I.e. data:image/jpeg;base64,/9j/4AA...
    a.rel = 'noopener'
    document.body.appendChild(a)
    click(a)
    document.body.removeChild(a)
}
reader.readAsDataURL(currentBlob)
alt text alt text

I have also tried changing the Base64 encoded string in several ways to try to rename the file, but to no effect.

reader.onloadend = () => {
    ...
    const url = reader.result
    const urlWithHeaders = url.replace(';', `;name=${currentFilename};headers=Content-Disposition%3A%20attachment%3B%20filename=%22${currentFilename}%22;`)
    const urlFile = url.replace(/^data:[^;]*;/, 'data:attachment/file;')
    const urlOctet = url.replace('image/jpeg', 'binary/octet-stream')
    a.href = url / urlWithHeaders / urlFile / urlOctet
    ...
}

Solution Attempt 2: Using a new window

I have also tried by opening a new window, but it just opens the image in a new window and does not prompt a download:

const currentBlob = new Blob([buffer], {type: 'image/jpeg'})
const newWindow = window.open(URL.createObjectURL(currentBlob), '_blank')

Questions

  • Is Chrome iOS downloading supported with FileSaver.js?
  • Is this a known limitation of Chrome iOS?

There are several issues that seem potentially related, but all of them are quite old and haven't focused on images specifically: #506, #576, #343

I have also tried with an mp4 but, just like these questions describe, it is not working:

Do you have a try to add the response header -----Content-Disposition:attachment;filename='xxx.xxx'?

av01d commented 3 years ago

This issue is still present today it seems? Latest Chrome on latest iPadOS. How can we set the filename of a file download in Chrome for iOS?

av01d commented 3 years ago

Do you have a try to add the response header -----Content-Disposition:attachment;filename='xxx.xxx'?

Do you understand the issue? How do you set a Content-Disposition header in client side javascript?

jparismorgan commented 3 years ago

@av01d I think @xiaoluntian is referring to something like urlWithHeaders, which if you read the Solution Attempt 1 section above I mention trying unsuccessfully:

I have also tried changing the Base64 encoded string in several ways to try to rename the file, but to no effect. reader.onloadend = () => { ... const url = reader.result const urlWithHeaders = url.replace(';', ;name=${currentFilename};headers=Content-Disposition%3A%20attachment%3B%20filename=%22${currentFilename}%22;) const urlFile = url.replace(/^data:[^;]*;/, 'data:attachment/file;') const urlOctet = url.replace('image/jpeg', 'binary/octet-stream') a.href = url / urlWithHeaders / urlFile / urlOctet ... }

But I don't remember much of what I tried beyond what is written above, so you may want to give it a go yourself, @av01d.

To answer your original question @xiaoluntian, yes, I tried setting a header and it didn't work 😕 . Is the code snippet above how you suggest doing it?

Gbahdeyboh commented 3 years ago

This issue still persists, anyone found a fix?

vanga commented 2 years ago

It is working in chrome after setting the Content Disposition response header.

jparismorgan commented 2 years ago

@vanga Could you share example code? Thanks!

vanga commented 2 years ago

@jparismorgan My images are stored in S3, so I just set the metadata so that content disposition response header is sent. I have set it to Content-disposition: attachment like suggested in few of the above comments in this thread.

Ismael-S commented 1 year ago

Found a solution for setting the filename and extension, (had this problem with various browsers when obtaining the base64 dynamically). I had to add target="_blank".

On Solution 1, before appending the element to the body, add: a.target = '_blank'

I'm not using this library, but my solution is the same as Solution 1, so hopefully it helps you.

jfederer commented 2 months ago

Is there a solution to this yet? It's been 4 years, I'm hoping something has happened.

Changing the delivery from s3 isn't a viable option, as I want it to display in the browser nomrally, but when they click a download button, I want to download the image.