Angular download PDF on IOS mostly opens in same tab - javascript

I'm using file-saver in my angular application to download a PDF generated in my backend. The library generally works fine on desktop and android. But I don't seem to be able to download a file on IOS. file-saver doesn't, as stated in on the GitHub page, open the blob in a new Page either. it jus opens on the same page (not wanted). Funnily enough it works fine in safari (it opens a dialog that asks me to download and then downloads it without opening it). In any other browser (opera, firefox and chrome) it doesn't seem to work.
I've tried file-saver, downloadJ, creating an anchor tag myself together with the download attribute, using the application/octet-stream mime-type and several other solutions posted on the internet. All of these methods in most browsers just doe nothing or open the PDF blob in the same page instead of downloading it or opening it in a new tab (as file-saver states it would do on IOS).
I'm generating the PDF in a Google Cloud Function. Is there maybe a way to skip the whole client side of things and make the browser download the file directly from there?
Does anyone have another idea on how to download PDF's on mobile IOS (e.g. with a service worker or something)?
Thanks in advance

Best solution as per new chrome specification https://developers.google.com/web/updates/2018/02/chrome-65-deprecations
Vanilla JavaScript
public static downloadFile(url: string): void {
const xmlHttp = new XMLHttpRequest();
xmlHttp.onreadystatechange = () => {
if (xmlHttp.readyState === 4 && xmlHttp.status === 200) {
const blobUrl = window.URL.createObjectURL(xmlHttp.response);
const e = document.createElement('a');
e.href = blobUrl;
e.download = blobUrl.substr(blobUrl.lastIndexOf('/') + 1);
document.body.appendChild(e);
e.click();
document.body.removeChild(e);
}
};
xmlHttp.responseType = 'blob';
xmlHttp.open('GET', url, true);
xmlHttp.send(null);
}
If you're using angular try this.
async downloadBrochure(url: string) {
try {
const res = await this.httpClient.get(url, { responseType: 'blob' }).toPromise();
this.downloadFile(res);
} catch (e) {
console.log(e.body.message);
}
}
downloadFile(data) {
const url = window.URL.createObjectURL(data);
const e = document.createElement('a');
e.href = url;
e.download = url.substr(url.lastIndexOf('/') + 1);
document.body.appendChild(e);
e.click();
document.body.removeChild(e);
}

Related

JavaScript: How to create and save text file [duplicate]

This question already has answers here:
How to create a file in memory for user to download, but not through server?
(22 answers)
Closed 2 years ago.
I have data that I want to write to a file, and open a file dialog for the user to choose where to save the file. It would be great if it worked in all browsers, but it has to work in Chrome. I want to do this all client-side.
Basically I want to know what to put in this function:
saveFile: function(data)
{
}
Where the function takes in data, has the user select a location to save the file, and creates a file in that location with that data.
Using HTML is fine too, if that helps.
A very minor improvement of the code by Awesomeness01 (no need for anchor tag) with addition as suggested by trueimage (support for IE):
// Function to download data to a file
function download(data, filename, type) {
var file = new Blob([data], {type: type});
if (window.navigator.msSaveOrOpenBlob) // IE10+
window.navigator.msSaveOrOpenBlob(file, filename);
else { // Others
var a = document.createElement("a"),
url = URL.createObjectURL(file);
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
setTimeout(function() {
document.body.removeChild(a);
window.URL.revokeObjectURL(url);
}, 0);
}
}
Tested to be working properly in Chrome, FireFox and IE10.
In Safari, the data gets opened in a new tab and one would have to manually save this file.
function download(text, name, type) {
var a = document.getElementById("a");
var file = new Blob([text], {type: type});
a.href = URL.createObjectURL(file);
a.download = name;
}
click here to download your file
<button onclick="download('file text', 'myfilename.txt', 'text/plain')">Create file</button>
And you would then download the file by putting the download attribute on the anchor tag.
The reason I like this better than creating a data url is that you don't have to make a big long url, you can just generate a temporary url.
This project on github looks promising:
https://github.com/eligrey/FileSaver.js
FileSaver.js implements the W3C saveAs() FileSaver interface in
browsers that do not natively support it.
Also have a look at the demo here:
http://eligrey.com/demos/FileSaver.js/
Choosing the location to save the file before creating it is not possible. But it is possible, at least in Chrome, to generate files using just JavaScript. Here is an old example of mine of creating a CSV file. The user will be prompted to download it. This, unfortunately, does not work well in other browsers, especially IE.
<!DOCTYPE html>
<html>
<head>
<title>JS CSV</title>
</head>
<body>
<button id="b">export to CSV</button>
<script type="text/javascript">
function exportToCsv() {
var myCsv = "Col1,Col2,Col3\nval1,val2,val3";
window.open('data:text/csv;charset=utf-8,' + escape(myCsv));
}
var button = document.getElementById('b');
button.addEventListener('click', exportToCsv);
</script>
</body>
</html>
For latest browser, like Chrome, you can use the File API as in this tutorial:
window.requestFileSystem = window.requestFileSystem || window.webkitRequestFileSystem;
window.requestFileSystem(window.PERSISTENT, 5*1024*1024 /*5MB*/, saveFile, errorHandler);
function SaveBlobAs(blob, file_name) {
if (typeof navigator.msSaveBlob == "function")
return navigator.msSaveBlob(blob, file_name);
var saver = document.createElementNS("http://www.w3.org/1999/xhtml", "a");
var blobURL = saver.href = URL.createObjectURL(blob),
body = document.body;
saver.download = file_name;
body.appendChild(saver);
saver.dispatchEvent(new MouseEvent("click"));
body.removeChild(saver);
URL.revokeObjectURL(blobURL);
}
Tried this in the console, and it works.
var aFileParts = ['<a id="a"><b id="b">hey!</b></a>'];
var oMyBlob = new Blob(aFileParts, {type : 'text/html'}); // the blob
window.open(URL.createObjectURL(oMyBlob));
You cannot do this purely in Javascript. Javascript running on browsers does not have enough permission yet (there have been proposals) due to security reasons.
Instead, I would recommend using Downloadify:
A tiny javascript + Flash library that enables the creation and download of text files without server interaction.
You can see a simple demo here where you supply the content and can test out saving/cancelling/error handling functionality.
For Chrome and Firefox, I have been using a purely JavaScript method.
(My application cannot make use of a package such as Blob.js because it is served from a special engine: a DSP with a WWWeb server crammed in and little room for anything at all.)
function FileSave(sourceText, fileIdentity) {
var workElement = document.createElement("a");
if ('download' in workElement) {
workElement.href = "data:" + 'text/plain' + "charset=utf-8," + escape(sourceText);
workElement.setAttribute("download", fileIdentity);
document.body.appendChild(workElement);
var eventMouse = document.createEvent("MouseEvents");
eventMouse.initMouseEvent("click", true, false, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null);
workElement.dispatchEvent(eventMouse);
document.body.removeChild(workElement);
} else throw 'File saving not supported for this browser';
}
Notes, caveats, and weasel-words:
I have had success with this code in both Chrome and Firefox clients running in Linux (Maipo) and Windows (7 and 10) environments.
However, if sourceText is larger than a MB, Chrome sometimes (only sometimes) gets stuck in its own download without any failure indication; Firefox, so far, has not exhibited this behavior. The cause might be some blob limitation in Chrome. Frankly, I just don't know; if anybody has any ideas how to correct (or at least detect), please post. If the download anomaly occurs, when the Chrome browser is closed, it generates a diagnostic such as
This code is not compatible with Edge or Internet Explorer; I have not tried Opera or Safari.
StreamSaver is an alternative to save very large files without having to keep all data in the memory.In fact it emulates everything the server dose when saving a file but all client side with service worker.
You can either get the writer and manually write Uint8Array's to it or pipe a binary readableStream to the writable stream
There is a few example showcasing:
How to save multiple files as a zip
piping a readableStream from eg Response or blob.stream() to StreamSaver
manually writing to the writable stream as you type something
or recoding a video/audio
Here is an example in it's simplest form:
const fileStream = streamSaver.createWriteStream('filename.txt')
new Response('StreamSaver is awesome').body
.pipeTo(fileStream)
.then(success, error)
If you want to save a blob you would just convert that to a readableStream
new Response(blob).body.pipeTo(...) // response hack
blob.stream().pipeTo(...) // feature reference
Javascript has a FileSystem API. If you can deal with having the feature only work in Chrome, a good starting point would be: http://www.html5rocks.com/en/tutorials/file/filesystem/.

Display embedded PDF in Internet Explorer 11 from binary string or base64

There is a 3rd party service which sends me a PDF file in either binary string or base64 encoded. Is there any possibility to display the PDF embedded in IE 11 using either binary string or base64 encoded.
From SO and other forums, I concluded that IE 11 supports data uri only for images and not PDF (I might be wrong) which rules out base64. So the only option left is to display from binary string. I am using it in a Node App but I do not have the option to first save the retrieved file to Node server and use static URL.
Please let me know if above is achievable in IE 11.
Currently I'm trying to use npm package of https://github.com/pipwerks/PDFObject. For Chrome & Firefox, I retrieve the base64 file and embed it using the above package and works fine.
This solution uses [pdf.js] 1
Key steps in rendering a base64 PDF using PDF.js library
First decode it using atob
Then initializing a Uint8Array using above decoded data
Abstract from express-pdfjs/scripts/App.js
let options = {
method: 'GET',
uri: 'http://localhost:5000/getBase64Pdf',
resolveWithFullResponse: true
}
rp(options)
.then((response) => {
if (response.statusCode !== 200) {
console.error('http not 200 but : ', response.statusCode)
} else {
console.info('connected successfully : ' + response.statusCode)
let pdfData = atob(response.body)
let uint8ArrayPdf = new Uint8Array(pdfData.length)
for (let i = 0; i < pdfData.length; i++) {
uint8ArrayPdf[i] = pdfData.charCodeAt(i)
}
let pdfjsframe = document.getElementById('pdfViewer');
pdfjsframe.contentWindow.PDFViewerApplication.open(uint8ArrayPdf);
}
})
pdfViewer is an iframe in index.html
<iframe id="pdfViewer" src="http://localhost:3000/express-pdfjs/pdfViewer/web/viewer.html" height="1600" width="850" />
Please find a sample implementation for this using React on client
side
# https://github.com/rohanray/so-pdf-base64
Following the discussion with #roray - I'm adding a slightly different solution on the menu here :)
First off: 1.not using base64 string for this (although possible). 2. I'm working on asp mvc 3. using viewer.html viewer of pdf.js
So, from the server/controller fetch file from db and return bites
public ActionResult LoadFile(int id)
{
var file = db.Files.Where(i => i.Id == id).FirstOrDefault();
return File(file.BinaryFile, MediaTypeNames.Application.Pdf, "Name.pdf");
}
In the view/html add an iframe with source unspecified (src="") alternatively can created it on the fly on page-load event.BTW, I tried object, embed and iframe with FF/Chrome/Edge/IE 11 and found iframe seems to work consistently across the board. Dont like iframe though :/
<iframe src="" name="printDoc" id="printDoc" width="800" height="1000" type="application/pdf"></iframe>
Finally, in your script tag. This can be done via Ajax just the same. Basically, soon as the document is ready call the server, get the file in bytes, convert to blob url, append the blob url to the relative location of '/web/viewer.html?file=' in your project and update the src="" attribute of your iframe.
$(document).ready(function myfunction() {
var xhr = new XMLHttpRequest();
// works for EDGE/Chrome/FFox
xhr.open("GET", "/Documents/LoadFile/" + fileId, true);
xhr.responseType = "blob";
xhr.onload = function (e) {
if (this.status === 200) {
var blob = new Blob([this.response], { type: 'application/pdf' });
console.log('Not IE 11');
var url = window.URL.createObjectURL(blob);
_iFrame = document.querySelector("#printDoc");
_iFrame.setAttribute('src', '/web/viewer.html?file=' + url);
}
};
xhr.send();
});
This will open the pdf embedded in the viewer.html embedded in the iframe. MS Edge works fine with this but not the blob url being sent as a query param over the address bar which obviously is supported by FF/Chrome. And, that's why I took this option.
PS. IE 11 remains hopeless for me so I used var blob = new Blob([this.response], {type: 'application/pdf' });window.navigator.msSaveOrOpenBlob(blob, fileName);Please let me know if you find a way to improve IE 11 performance

Large blob file in Javascript

I have an XHR object that downloads 1GB file.
function getFile(callback)
{
var xhr = new XMLHttpRequest();
xhr.onload = function () {
if (xhr.status == 200) {
callback.apply(xhr);
}else{
console.log("Request error: " + xhr.statusText);
}
};
xhr.open('GET', 'download', true);
xhr.onprogress = updateProgress;
xhr.responseType = "arraybuffer";
xhr.send();
}
But the File API can't load all that into memory even from a worker
it throws out of memory...
btn.addEventListener('click', function() {
getFile(function() {
var worker = new Worker("js/saving.worker.js");
worker.onmessage = function(e) {
saveAs(e.data); // FileSaver.js it creates URL from blob... but its too large
};
worker.postMessage(this.response);
});
});
Web Worker
onmessage = function (e) {
var view = new DataView(e.data, 0);
var file = new File([view], 'file.zip', {type: "application/zip"});
postMessage('file');
};
I'm not trying to compress the file, this file is already compressed from server.
I thought storing it first on indexedDB but i i'll have to load blob or file anyway, even if i do request by range bytes, soon or late i will have to build this giant blob..
I want to create blob: url and send it to user after been downloaded by browser
I'll use FileSystem API for Google Chrome, but i want make something for firefox, i looked into File Handle Api but nothing...
Do i have to build an extension for firefox, in order to do the same thing as FileSystem does for google chrome?
Ubuntu 32 bits
Loading 1gb+ with ajax isn't convenient just for monitoring download progress and filling up the memory.
Instead I would just send the file with a Content-Disposition header to save the file.
There are however ways to go around it to monitor the progress. Option one is to have a second websocket that signals how much you have downloaded while you are downloading normally with a get request. the other option will be described later in the bottom
I know you talked about using Blinks sandboxed filesystem in the conversation. but it has some drawbacks. It may need permission if using persistent storage. It only allows 20% of the available disk that are left. And if chrome needs to free some space then it will throw away any others domains temporary storage that was last used for the most recent file. Beside it doesn't work in private mode.
Not to mention that it has been dropping support for it and may never end up in other browsers - but they will most likely not remove it since many sites still depend on it
The only way to process this large file is with streams. That is why I have created a StreamSaver. This is only going to work in Blink (chrome & opera) ATM but it will eventually be supported by other browsers with the whatwg spec to back it up as a standard.
fetch(url).then(res => {
// One idea is to get the filename from Content-Disposition header...
const size = ~~res.headers.get('Content-Length')
const fileStream = streamSaver.createWriteStream('filename.zip', size)
const writeStream = fileStream.getWriter()
// Later you will be able to just simply do
// res.body.pipeTo(fileStream)
// instead of pumping
const reader = res.body.getReader()
const pump = () => reader.read()
.then(({ value, done }) => {
// here you know how large the value (chunk) is and you can
// figure out the download speed/progress when comparing it to the size
return done
? writeStream.close()
: writeStream.write(value).then(pump)
)
// Start the reader
pump().then(() =>
console.log('Closed the stream, Done writing')
)
})
This will not take up any memory
I have a theory that is if you split the file into chunks and store them in the indexedDB and then later merge them together it will work
A blob isn't made of data... it's more like pointers to where a file can be read from
Meaning if you store them in indexedDB and then do something like this (using FileSaver or alternative)
finalBlob = new Blob([blob_A_fromDB, blob_B_fromDB])
saveAs(finalBlob, 'filename.zip')
But i can't confirm this since i haven't tested it, would be good if someone else could
Blob is cool until you want to download a large file, there is a 600MB limit(chrome) for blob since it stores everything in memory.

How to show save file dialog in Safari?

I need help. I have an angular app and by using DocRaptor want to generate PDF and save it as file. But I cant trigger the dialog to save file in Safari with any method what I have found on Stack Overflow. Those methods open file in current browser tab and replace site html or open file in new tab. No one cant shows the dialog. Here the examples what I have already tried to use. Environment MacOS - EL Capitan. Safari 9.0.3
Solution #1
var content = 'file content for example';
var blob = new Blob([ content ], { type : 'text/plain' });
$scope.url = (window.URL || window.webkitURL).createObjectURL( blob );
Example jsfiddle. Shows file in current tab. Replaces site. But works in Chrome.
Solution #2
<a target="_self" href="mysite.com/uploads/ahlem.pdf" download="foo.pdf">
Example jsfiddle. Doesnt work at all in Safari. Works in Chrome.
Solution #3
<a class="btn" ng-click="saveJSON()" ng-href="{{ url }}">Export to JSON</a>
and
$scope.saveJSON = function () {
$scope.toJSON = '';
$scope.toJSON = angular.toJson($scope.data);
var blob = new Blob([$scope.toJSON], { type:"application/json;charset=utf-8;" });
var downloadLink = angular.element('<a></a>');
downloadLink.attr('href',window.URL.createObjectURL(blob));
downloadLink.attr('download', 'fileName.json');
downloadLink[0].click();
};
Example Code Snippet. Shows the file content instead of document's html.
Solution #4
function download(text, name, type) {
var a = document.getElementById("a");
var file = new Blob([text], {type: type});
a.href = URL.createObjectURL(file);
a.download = name;
}
Example Code Snippet. Replace document with file content in Safari. Works in Chrome.
And similar Solution #5
function download(text, name, type) {
var a = document.createElement("a");
var file = new Blob([text], {type: type});
a.href = URL.createObjectURL(file);
a.download = name;
a.click();
}
Example jsfiddle. Doesnt work at all in Safari. Works in Chrome.
Also I have tried to use libraries like:
FileSaver - It opens file in Safari instead of document. So you should click Cmd+S. Example.
If we use type 'pplication/octet-stream' the name of file will be unknown or there was be an error 'Failed to load resource: Frame load interrupted'. Issue.
Second library Downloadify - doesnt work in Safari at all. Issue.
Angular library nw-fileDialog - instead of save as it shows choose file. Issue.
DocRaptor has own example with jQuery.
Example with angular in jsfiddle. It works in Chrome but in Safari example doesnt work be cause of error with SAMEORIGIN
Refused to display 'https://docraptor.com/docs' in a frame because it set 'X-Frame-Options' to 'SAMEORIGIN'.
But if we reproduce it on server and change url on 'https://docraptor.com/docs.pdf' it works and open file in new tab and automatically download the file so you cant choose a folder and after download user see white empty screen tab in browser. If we specify form target="_self" it will work perfect, but console will have an error 'Failed to load resource:'.
I will appreciate any help with this problem.
Thanks.
Regards.
Try using Blob file for this:
// Buffer can be response from XMLHttpRequest/Ajax or your custom Int32 data
function download(buffer, filename) {
var file = new Blob([buffer], {
type: 'application/octet-stream' // Replace your mimeType if known
});
var fileReader = new FileReader();
fileReader.onloadend = function(e) {
var converted = e.target.result;
converted.name = filename;
converted.webkitRelativePath = filename;
var iframe = document.createElement('iframe');
iframe.style.display = 'none';
document.body.appendChild(iframe);
iframe.src = converted;
};
fileReader.onerror = function(e) {
throw new Error('Something is wrong with buffer data');
};
fileReader.file = file;
fileReader.readAsDataURL(file);
}
It basically uses filebuffer and download that as an iframe content. Make sure to hook correct mime type so that safari security system will recieved analyse filetype.
Ideally, Solution #2 would be the answer, but the download attribute does not yet have cross-browser support.
So you have to use a <form> to create the download. As you noted, DocRaptor's jQuery example uses this technique.
The SAMEORIGIN error is actually because JSFiddle is running the code in an iFrame with their origin settings. If you run this straight from your Angular application, you shouldn't have any problems.

Fetching BLOB in Chrome Android

I'm struggling to fetch an HTML5 video using xhr2 and blob responseType with Chrome on Android 4.2. The code works perfectly on Chrome and Firefox desktop and on Firefox Android 4.2 (with FF desktop, I use a webm video instead of the mp4).
// Taking care of prefix
window.URL = window.URL || window.webkitURL;
// This function download the video
var loadVideo = function() {
var xhr = new XMLHttpRequest();
xhr.addEventListener('load', addVideoFile, false);
xhr.open('GET', "videos/myvideo.mp4" , true);
xhr.responseType = 'blob';
xhr.send();
};
// this function sets the video source
var addVideoFile = function() {
if(4 == this.readyState && 200 == this.status) {
var video = document.getElementById('vid'),
blob = this.response;
video.src = window.URL.createObjectURL(blob);
console.log('video ready');
}
};
loadVideo();
Can anyone explain me why this does not work with Chrome on Android? If I plug my phone to use the remote debugging, the console will display 'video ready', suggesting that the video was downloaded but it's impossible to play it, video is just a black screen.
Also, this code works if I use it to fetch images instead of video. Is there a limitation I'm not aware of, preventing to download Blob above a certain size? (My video is 1.5 MB).
Thanks you very much for your help!
This is most certainly a bug. If you get something that works on Desktop Chrome but not Android then 99.5% of the time it is an issue we need to fix.
I have replicated your issue http://jsbin.com/uyehun/1 and I have filed the bug too https://code.google.com/p/chromium/issues/detail?id=253465
Per http://caniuse.com/bloburls, for Android 4.0-4.3 you need to use window.webkitURL.createObjectUrl() instead of window.URL.createObjectUrl().
This will let you generate a blob url, though I haven't actually been able to get a video element to play such an url.

Categories

Resources