Alternative for 'download' attribute in Safari/iOS - javascript

I have a blob created with a base64, and I need to make this data downloadable as a pdf.
I created this snippet:
var blob = new Blob([byte]);
var link = document.createElement('a');
link.href = window.URL.createObjectURL(blob);
link.target = '_blank';
var fileName = name + '.pdf';
link.download = fileName;
link.click();
It works on all the browsers, except safari mobile on iOS.
The file gets actually downloaded, but its name is "unknown", then it can't be open since the extension gets lost.
The problem is that the download attribute lacks support on this browser and IE.
There are a lot of workarounds for IE, but I didn't find any for Safari/iOS.
Do you know how can I download a blob got from a base64 (no XHR involved) in this browser?
Thank you

I need to make this data downloadable as a pdf (...) in safari iOS
SHORT ANSWER: you can't. Due this bug is impossible to download the file on safari iOS
The alternative is to open the file on the browser with the proper mime type, so it can show its content (and the user can then manually download it if needed).
Make sure to pass mime type when creating the Blob. reference
var blob = new Blob([byte], {type: 'application/pdf'});
Lastly, I'd strongly suggest you to use FileSaver.js which that can handle most of the corner cases/multiple browser support for save (or in this case, open) a file in javascript.

As per the below link:-
https://caniuse.com/#feat=download
Safari 13 Beta 3 is released so you can check on the same, whether its working or not?
You can download a blob got from a base64 by using a atob function.
The atob function will decode a base64-encoded string into a new string with a character for each byte of the binary data.
You can save blob locally via FileSaver.js .
You can also check here that would be helpful:-
How to open Blob URL on Chrome iOS

This is something I have tried in my project and it is working for me.
import "./styles.css";
var pdfData =
"JVBERi0xLjcKCjEgMCBvYmogICUgZW50cnkgcG9pbnQKPDwKICAvVHlwZSAvQ2F0YWxvZwog" +
"IC9QYWdlcyAyIDAgUgo+PgplbmRvYmoKCjIgMCBvYmoKPDwKICAvVHlwZSAvUGFnZXMKICAv" +
"TWVkaWFCb3ggWyAwIDAgMjAwIDIwMCBdCiAgL0NvdW50IDEKICAvS2lkcyBbIDMgMCBSIF0K" +
"Pj4KZW5kb2JqCgozIDAgb2JqCjw8CiAgL1R5cGUgL1BhZ2UKICAvUGFyZW50IDIgMCBSCiAg" +
"L1Jlc291cmNlcyA8PAogICAgL0ZvbnQgPDwKICAgICAgL0YxIDQgMCBSIAogICAgPj4KICA+" +
"PgogIC9Db250ZW50cyA1IDAgUgo+PgplbmRvYmoKCjQgMCBvYmoKPDwKICAvVHlwZSAvRm9u" +
"dAogIC9TdWJ0eXBlIC9UeXBlMQogIC9CYXNlRm9udCAvVGltZXMtUm9tYW4KPj4KZW5kb2Jq" +
"Cgo1IDAgb2JqICAlIHBhZ2UgY29udGVudAo8PAogIC9MZW5ndGggNDQKPj4Kc3RyZWFtCkJU" +
"CjcwIDUwIFRECi9GMSAxMiBUZgooSGVsbG8sIHdvcmxkISkgVGoKRVQKZW5kc3RyZWFtCmVu" +
"ZG9iagoKeHJlZgowIDYKMDAwMDAwMDAwMCA2NTUzNSBmIAowMDAwMDAwMDEwIDAwMDAwIG4g" +
"CjAwMDAwMDAwNzkgMDAwMDAgbiAKMDAwMDAwMDE3MyAwMDAwMCBuIAowMDAwMDAwMzAxIDAw" +
"MDAwIG4gCjAwMDAwMDAzODAgMDAwMDAgbiAKdHJhaWxlcgo8PAogIC9TaXplIDYKICAvUm9v" +
"dCAxIDAgUgo+PgpzdGFydHhyZWYKNDkyCiUlRU9G";
let download = () => {
if (pdfData) {
var iOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
var isChrome =
navigator.userAgent.toLowerCase().indexOf("CriOS") > -1 ||
navigator.vendor.toLowerCase().indexOf("google") > -1;
var iOSVersion = [];
if (iOS) {
iOSVersion = navigator.userAgent
.match(/OS [\d_]+/i)[0]
.substr(3)
.split("_")
.map((n) => parseInt(n));
}
var attachmentData = pdfData;
var attachmentName = "Test.pdf";
var contentType = "application/pdf";
var binary = atob(attachmentData.replace(/\s/g, ""));
var len = binary.length;
var buffer = new ArrayBuffer(len);
var view = new Uint8Array(buffer);
for (var i = 0; i < len; i++) {
view[i] = binary.charCodeAt(i);
}
var linkElement = document.createElement("a");
try {
var hrefUrl = "";
var blob = "";
if (iOS && !isChrome && iOSVersion[0] <= 12) {
blob = "data:application/pdf;base64," + pdfData;
hrefUrl = blob;
} else {
if (iOS && !isChrome) {
contentType = "application/octet-stream";
}
blob = new Blob([view], { type: contentType });
hrefUrl = window.URL.createObjectURL(blob);
}
linkElement.setAttribute("href", hrefUrl);
linkElement.setAttribute("target", "_blank");
if ((iOS && (iOSVersion[0] > 12 || isChrome)) || !iOS) {
linkElement.setAttribute("download", attachmentName);
}
var clickEvent = new MouseEvent("click", {
view: window,
bubbles: true,
cancelable: false
});
linkElement.dispatchEvent(clickEvent);
} catch (ex) {}
}
};
export default function App() {
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<button onClick={download}>Download</button>
</div>
);
}

The "target" attribute in Safari seems to override the "download" attribute. Currently, as to my knowledge, there is no way to solve this. So I think you have to wait for the next Safari version (13) which will be out in a few months.

Related

Problems downloading big file(max 15 mb) on google chrome

I have a downloading problem in Google Chrome.
I am using Ruby 2.2, Rails 4.2, AngularJS 1.2.
We dont have a database here. Everything we are getting through API. The file which we are trying to download is around 7 mb. It gives us "Failed: Network Error". Though it works fine on Firefox.
From the API we are getting binary data in JSON. We are parsing it. And then:
send_data response_fields["attachment"], type: response_fields["mimeType"], disposition: 'attachment', filename: params[:filename]
As we are using AngularJS, we are catching that value in AngularJS Controller and then converting it as:
var str = data;
var uri = "data:" + mimeType + ";base64," + str;
var downloadLink = document.createElement("a");
downloadLink.href = uri;
downloadLink.download = filename;
document.body.appendChild(downloadLink);
downloadLink.click();
document.body.removeChild(downloadLink);
This works in Firefox & even Chrome for smaller file size. Not sure why it is giving error for bigger size on Chrome.
Any suggestions?
Thanks.
This is an almost duplicate of these questions 1 and 2, but since they do deal particularly with the canvas element, I'll rewrite a more global solution here.
This problem is due to a size limit chrome has set in the anchor (<a>) download attribute. I'm not quite sure why they did it, but the solution is pretty easy.
Convert your dataURI to a Blob, then create an ObjectURL from this Blob, and pass this ObjectURL as the anchor's download attribute.
// edited from https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toBlob#Polyfill
function dataURIToBlob(dataURI) {
var binStr = atob(dataURI.split(',')[1]),
len = binStr.length,
arr = new Uint8Array(len),
mimeString = dataURI.split(',')[0].split(':')[1].split(';')[0]
for (var i = 0; i < len; i++) {
arr[i] = binStr.charCodeAt(i);
}
return new Blob([arr], {
type: mimeString
});
}
var dataURI_DL = function() {
var dataURI = this.result;
var blob = dataURIToBlob(dataURI);
var url = URL.createObjectURL(blob);
var blobAnchor = document.getElementById('blob');
var dataURIAnchor = document.getElementById('dataURI');
blobAnchor.download = dataURIAnchor.download = 'yourFile.mp4';
blobAnchor.href = url;
dataURIAnchor.href = dataURI;
stat_.textContent = '';
blobAnchor.onclick = function() {
requestAnimationFrame(function() {
URL.revokeObjectURL(url);
})
};
};
// That may seem stupid, but for the sake of the example, we'll first convert a blob to a dataURI...
var start = function() {
stat_.textContent = 'Please wait while loading...';
var xhr = new XMLHttpRequest();
xhr.responseType = 'blob';
xhr.onload = function() {
status.textContent = 'converting';
var fr = new FileReader();
fr.onload = dataURI_DL;
fr.readAsDataURL(this.response);
};
xhr.open('GET', 'https://dl.dropboxusercontent.com/s/bch2j17v6ny4ako/movie720p.mp4?dl=0');
xhr.send();
confirm_btn.parentNode.removeChild(confirm_btn);
};
confirm_btn.onclick = start;
<button id="confirm_btn">Start the loading of this 45Mb video</button>
<span id="stat_"></span>
<br>
<a id="blob">blob</a>
<a id="dataURI">dataURI</a>
And a jsfiddle version for FF, since they don't allow the downloadattribute from stack-snippets...

Open PDF in a new Tab in Safari

Help me I am in desperate need for help. Now to the real issue.
I have been trying to get my pdf to open up in new tab in Safari but till now I've only faced disappointment. I am using jsPDF for generating my PDFs and earlier I used it's
doc.save(pdfName+".pdf")
method to do the same, as in the latest safari version "9.1.1(10601.6.17)" it broke. I can still generate the pdf using jsPDF's inbuilt option:
pdf.output('dataurl');
but, it tends to open the PDF in the same tab, and this behaviour is highly undesirable. I tried various solutions available on the net to fix the issue but none works. Some of the solutions that I tried are :
Using window.open(), doesn't work.
Using location.href, doesn't work.
Using doc.output('save', 'filename.pdf'), doc.output('datauri'), doc.output('dataurlnewwindow'), nothing works.
Also tried fake clicking by creating an anchor tag and setting it's target as '_blank' and then using eventDispatcher to stimulate a mouse click but it also didn't work. Something like this :
var a = window.document.createElement("a");
a.target = '_blank';
a.href = 'http://www.google.com';
var e = window.document.createEvent("MouseEvents");
e.initMouseEvent("click", true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null);
a.dispatchEvent(e);
Any suggestion on what I should do ? I want my pdf to open up in a new tab and not in the same one as that of my application.
P.S : Each of the solutions that I tried for Safari above work flawlessly in Chrome and Mozilla Firefox, I just can't figure out what's the deal with Safari. Any help would be appreciated.
Tried many solution mentioned in stackoverflow but nothing worked for me, Later checked the FileSave.js implementation and created a working solution for this issue.
Note that I have successfully tested this solution in iOS Safari.
In function downloadPdfMobile the if part is for Web safari else part is for mobile safari.
Normal Android chrome browser this will not work, you need to follow the traditional way.
First you need to get the base64 string from the jsPDF
const doc = new jsPDF.jsPDF({ unit: 'pt', putOnlyUsedFonts: true});
const pdfElement = document.getElementById('id');
var pdfName = 'Test.pdf';
doc.html(pdfElement, {
callback: (pdf) => {
var base = pdf.output('datauristring').split(',')[1];
downloadPdfMobile(pdfName, base);
},
margin: 20,
});
This is how the downloadPdfMobile function looks like, First you need to convert the base64 t Blob.
downloadPdfMobile: function (FileName, content) {
var _global =
typeof window === "object" && window.window === window
? window
: typeof self === "object" && self.self === self
? self
: typeof global === "object" && global.global === global
? global
: this;
var isMacOSWebView =
_global.navigator && /Macintosh/.test(navigator.userAgent) && /AppleWebKit/.test(navigator.userAgent) && !/Safari/.test(navigator.userAgent);
var blob = this.base64toBlob(content);
var popup = open("", "_blank");
if (popup) {
popup.document.title = popup.document.body.innerText = "downloading...";
}
var force = blob.type === "application/octet-stream";
var isSafari = /constructor/i.test(_global.HTMLElement) || _global.safari;
var isChromeIOS = /CriOS\/[\d]+/.test(navigator.userAgent);
if ((isChromeIOS || (force && isSafari) || isMacOSWebView) && typeof FileReader !== "undefined") {
var reader = new FileReader();
reader.onloadend = function () {
var url = reader.result;
url = isChromeIOS ? url : url.replace(/^data:[^;]*;/, "data:attachment/file;");
if (popup) popup.location.href = url;
else location = url;
popup = null;
};
reader.readAsDataURL(blob);
} else {
var URL = _global.URL || _global.webkitURL;
var url = URL.createObjectURL(blob);
if (popup) popup.location = url;
else location.href = url;
popup = null;
setTimeout(function () {
URL.revokeObjectURL(url);
}, 4e4);
}
}
base64toBlob: function (base64str) {
var binary = atob(base64str.replace(/\s/g, ""));
var len = binary.length;
var buffer = new ArrayBuffer(len);
var view = new Uint8Array(buffer);
for (var i = 0; i < len; i++) {
view[i] = binary.charCodeAt(i);
}
return new Blob([view], {
type: "application/pdf",
});
},
If you can get your PDF as base64, this is how you can do with it (works on both Safari and Chrome)
let base64PDF = "JVBERi0xLjQKMSAwIG9iago8PAovVGl0bGUgKP....."// your base64 pdf data
let pdfWindow = window.open("", "_blank");
pdfWindow.document.write(
"<title>Healthcare</title><iframe width='100%' style='margin: -8px;border: none;' height='100%' src='data:application/pdf;base64, " +
encodeURI(base64PDF) +
"'></iframe>"
);
You could try the blob output. Then generate a Data URI to this, and open it in a new window:
var blob = pdf.output("blob");
window.open(URL.createObjectURL(blob));

Client download of a server generated zip file

Before somebody says, "duplicate", I just want to make sure, that folks know, that I have already reviewed these questions:
1) Uses angular and php, not sure what is happening here (I don't know PHP): Download zip file and trigger "save file" dialog from angular method
2) Can't get this answer to do anything: how to download a zip file using angular
3) This person can already download, which is past the point I'm trying to figure out:
Download external zip file from angular triggered on a button action
4) No answer for this one:
download .zip file from server in nodejs
5) I don't know what language this even is:
https://stackoverflow.com/questions/35596764/zip-file-download-using-angularjs-directive
Given those questions, if this is still a duplicate, I apologize. Here is, yet, another version of this question.
My angular 1.5.X client gives me a list of titles, of which each have an associated file. My Node 4.X/Express 4.X server takes that list, gets the file locations, creates a zip file, using express-zip from npm, and then streams that file back in the response. I then want my client to initiate the browser's "download a file" option.
Here's my client code (Angular 1.5.X):
function bulkdownload(titles){
titles = titles || [];
if ( titles.length > 0 ) {
$http.get('/query/bulkdownload',{
params:{titles:titles},
responseType:'arraybuffer'
})
.then(successCb,errorCb)
.catch(exceptionCb);
}
function successCb(response){
// This is the part I believe I cannot get to work, my code snippet is below
};
function errorCb(error){
alert('Error: ' + JSON.stringify(error));
};
function exceptionCb(ex){
alert('Exception: ' + JSON.stringify(ex));
};
};
Node (4.X) code with express-zip, https://www.npmjs.com/package/express-zip:
router.get('/bulkdownload',function(req,resp){
var titles = req.query.titles || [];
if ( titles.length > 0 ){
utils.getFileLocations(titles).
then(function(files){
let filename = 'zipfile.zip';
// .zip sets Content-Type and Content-disposition
resp.zip(files,filename,console.log);
},
_errorCb)
}
});
Here's my successCb in my client code (Angular 1.5.X):
function successCb(response){
var URL = $window.URL || $window.webkitURL || $window.mozURL || $window.msURL;
if ( URL ) {
var blob = new Blob([response.data],{type:'application/zip'});
var url = URL.createObjectURL(blob);
$window.open(url);
}
};
The "blob" part seems to work fine. Checking it in IE's debugger, it does look like a file stream of octet information. Now, I believe I need to get that blob into the some HTML5 directive, to initiate the "Save File As" from the browser. Maybe? Maybe not?
Since 90%+ of our users are using IE11, I test all of my angular in PhantomJS (Karma) and IE. When I run the code, I get the old "Access is denied" error in an alert window:
Exception: {"description":"Access is denied...<stack trace>}
Suggestions, clarifications, answers, etc. are welcome!
Use this one:
var url="YOUR ZIP URL HERE";
window.open(url, '_blank');
var zip_file_path = "" //put inside "" your path with file.zip
var zip_file_name = "" //put inside "" file name or something
var a = document.createElement("a");
document.body.appendChild(a);
a.style = "display: none";
a.href = zip_file_path;
a.download = zip_file_name;
a.click();
document.body.removeChild(a);
As indicated in this answer, I have used the below Javascript function and now I am able to download the byte[] array content successfully.
Function to convert byte array stream (type of string) to blob object:
var b64toBlob = function(b64Data, contentType, sliceSize) {
contentType = contentType || '';
sliceSize = sliceSize || 512;
var byteCharacters = atob(b64Data);
var byteArrays = [];
for (var offset = 0; offset < byteCharacters.length; offset += sliceSize) {
var slice = byteCharacters.slice(offset, offset + sliceSize);
var byteNumbers = new Array(slice.length);
for (var i = 0; i < slice.length; i++) {
byteNumbers[i] = slice.charCodeAt(i);
}
var byteArray = new Uint8Array(byteNumbers);
byteArrays.push(byteArray);
}
var blob = new Blob(byteArrays, {type: contentType});
return blob;
};
An this is how I call this function and save the blob object with FileSaver.js (getting data via Angular.js $http.get):
$http.get("your/api/uri").success(function(data, status, headers, config) {
//Here, data is type of string
var blob = b64toBlob(data, 'application/zip');
var fileName = "download.zip";
saveAs(blob, fileName);
});
Note: I am sending the byte[] array (Java-Server-Side) like this:
byte[] myByteArray = /*generate your zip file and convert into byte array*/ new byte[]();
return new ResponseEntity<byte[]>(myByteArray , headers, HttpStatus.OK);
I updated my bulkdownload method to use $window.open(...) instead of $http.get(...):
function bulkdownload(titles){
titles = titles || [];
if ( titles.length > 0 ) {
var url = '/query/bulkdownload?';
var len = titles.length;
for ( var ii = 0; ii < len; ii++ ) {
url = url + 'titles=' + titles[ii];
if ( ii < len-1 ) {
url = url + '&';
}
}
$window.open(url);
}
};
I have only tested this in IE11.

Saving Base64 encoded PDF with Internet Explorer 10 and below

I have recently started using Bootstrap Table and the export plugin for it. A problem I came across is that the export plugin doesn't work for IE, because the files are output using Data URI, which IE doesn't support (I think I read that IE 11 supports it, but no one in my organization uses IE 11, of course.).
The only file types I am interested in exporting are Microsoft office types (Word, Excel) and PDFs.
I have created a functioning workaround for the MS files.
if (supportsDataUriNavigation())
window.open('data:application/vnd.ms-' + defaults.type + ';filename=exportData.doc;' + base64data);
else {
var iframe = document.getElementById("dataFrame");
iframe = iframe.contentWindow || iframe.contentDocument;
iframe.document.open("text/html", "replace");
iframe.document.write(excelFile);
iframe.document.close();
iframe.focus();
iframe.document.execCommand("SaveAs", true, defaults.fileName + fileExtension);
}
In this check, if the browser is IE, then I use the code I found here. This works great for the MS file types, but when I try to do the same for the PDF file, it tries to save as a .htm file. This file is blank. If I replace the fileExtension variable with .pdf in my code, the SaveAs window will not display when I click to export. There are no javascript errors.
I set the src attribute of an embed tag to a string that is returned by the plugin javascript, and it looks like so: 'application/pdf;base64,' + Base64.encode(buffer)
Anyone know of any solution/workaround?
I know this is very old question, but I found the solution and I want to share anyone who has the same problem. You can see the demo here :
https://jsfiddle.net/quangminh_ln/hy36tnt6/
'use strict';
var data = "...Your PDF base64 string...";
var fileName = "your_file_name";
if (window.navigator && window.navigator.msSaveOrOpenBlob) { // IE workaround
var byteCharacters = atob(data);
var byteNumbers = new Array(byteCharacters.length);
for (var i = 0; i < byteCharacters.length; i++) {
byteNumbers[i] = byteCharacters.charCodeAt(i);
}
var byteArray = new Uint8Array(byteNumbers);
var blob = new Blob([byteArray], {type: 'application/pdf'});
window.navigator.msSaveOrOpenBlob(blob, fileName);
}
else { // much easier if not IE
window.open("data:application/pdf;base64, " + data, '', "height=600,width=800");
}
The link that I saw for my solution : https://viethoblog.wordpress.com/2016/08/30/loaddisplay-pdf-from-base64-string-bonus-ie-workaround/

Javascript to Download String

Trying to initiate a browser download in Javascript, but the data I want to be downloaded is in a string, not a file. I know if it were a file, the following would do it:
window.location.href = '/filepath/file.csv';
How can I get this same effect, only with a string (with csv data), not a file that already exists on the server?
using my handy downloader:
<script src="http://danml.com/js/download.js"></script>
<script>download("hello world", "hello.txt", "text/plain")</script>
you can do it without a library as well, though my "lib" isn't very big and supports older FF+CH and IE10:
<a id=dl download="file.txt">Download</a>
<script>
content=prompt("enter contents");
dl.href="data:text/plain,"+encodeURIComponent(content);
dl.click();
</script>
EDIT: the linked script now supports window.URL.createObjectURL() for downloading files that were too big using dataURLs. I don't know the new limit, but 10mb works just file, whereas ~2mb is a limit for many dataURL ( window.open/A[download] - based ) solutions3
Below is a function I have writen in the past to handle such behavior (it may require some tweaking):
var downloadFile = function (filename, dataValue) {
window.URL = window.webkitURL || window.URL;
window.BlobBuilder = window.BlobBuilder || window.WebKitBlobBuilder;
var prevLink = output.querySelector('a');
if (prevLink) {
window.URL.revokeObjectURL(prevLink.href);
output.innerHTML = '';
}
var a = document.createElement('a');
a.download = '" + filename + #".csv';
if (BlobBuilder == undefined) {
var bb = new Blob([dataValue], { 'type': MIME_TYPE });
a.href = window.URL.createObjectURL(bb);
}
else {
var bb = new BlobBuilder();
bb.append(dataValue);
a.href = window.URL.createObjectURL(bb.getBlob(MIME_TYPE));
}
a.textContent = 'Download ready';
a.dataset.downloadurl = [MIME_TYPE, a.download, a.href].join(':');
a.draggable = true; // Don't really need, but good practice.
a.classList.add('dragout');
output.appendChild(a);
a.onclick = function (e) {
if ('disabled' in this.dataset) {
return false;
}
};
};

Categories

Resources