Downloaded PDF looks empty although it contains some data - javascript

I'm trying to implement a PDF file download functionality with JavaScript.
As a response to a POST request I get a PDF file, in Chrome DevTools console it looks like (the oResult data container, fragment):
"%PDF-1.4↵%����↵4 0 obj↵<</Filter/FlateDecode/Length 986>>stream↵x��
Now I'm trying to initialize the download process:
let blob = new Blob([oResult], {type: "application/pdf"});
let link = document.createElement('a');
link.href = window.URL.createObjectURL(blob);
link.download = "tstPDF";
link.click();
As a result, upon a click on a button I get tstPDF.pdf, it contains the correct number of pages, but the PDF itself is empty, no content is displayed, although it is 6 KB.
When I test the Java server-side module, which generates the PDF, everything is working fine, it sends InputStream through ServletOutputStream. Thus I assume that the issue is somewhere on a client side, perhaps something with MIME, BLOB, encoding, or similar.
Why doesn't the generated PDF display any data?

I solved the issue.
The problem was in a way the data is delivered from the server to the client.
It is critical to assure that the server sends the data in Base64 encoding, otherwise the client side can't deserialize the PDF string back to the binary format. Below, you can find the full solution.
Server-side:
OutputStream pdfStream = PDFGenerator.pdfGenerate(data);
String pdfFileName = "test_pdf";
// represent PDF as byteArray for further serialization
byte[] byteArray = ((java.io.ByteArrayOutputStream) pdfStream).toByteArray();
// serialize PDF to Base64
byte[] encodedBytes = java.util.Base64.getEncoder().encode(byteArray);
response.reset();
response.addHeader("Pragma", "public");
response.addHeader("Cache-Control", "max-age=0");
response.setHeader("Content-disposition", "attachment;filename=" + pdfFileName);
response.setContentType("application/pdf");
// avoid "byte shaving" by specifying precise length of transferred data
response.setContentLength(encodedBytes.length);
// send to output stream
ServletOutputStream servletOutputStream = response.getOutputStream();
servletOutputStream.write(encodedBytes);
servletOutputStream.flush();
servletOutputStream.close();
Client side:
let binaryString = window.atob(data);
let binaryLen = binaryString.length;
let bytes = new Uint8Array(binaryLen);
for (let i = 0; i < binaryLen; i++) {
let ascii = binaryString.charCodeAt(i);
bytes[i] = ascii;
}
let blob = new Blob([bytes], {type: "application/pdf"});
let link = document.createElement('a');
link.href = window.URL.createObjectURL(blob);
link.download = pdfFileName;
link.click();
Reference topics:
How to convert a PDF generating in response.outputStream to a Base64 encoding
Download File from Bytes in JavaScript

Thanks to this. It really works.
BTW, here's how I do it using spring controller and ajax with pdf generated by jasper
The Controller:
public ResponseEntity<?> printPreview(#ModelAttribute("claim") Claim claim)
{
try
{
//Code to get the byte[] from jasper report.
ReportSource source = new ReportSource(claim);
byte[] report = reportingService.exportToByteArrayOutputStream(source);
//Conversion of bytes to Base64
byte[] encodedBytes = java.util.Base64.getEncoder().encode(report);
//Setting Headers
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.parseMediaType("application/pdf"));
headers.setContentDispositionFormData("pdfFileName.pdf", "pdfFileName.pdf");
headers.setCacheControl("must-revalidate, post-check=0, pre-check=0");
headers.setContentLength(encodedBytes.length);
return new ResponseEntity<>(encodedBytes, headers, HttpStatus.OK);
}
catch (Exception e)
{
LOG.error("Error on generating report", e);
return new ResponseEntity<>(null, HttpStatus.INTERNAL_SERVER_ERROR);
}
}
The ajax:
$.ajax({
type: "POST",
url: "",
data: form.serialize(), //Data from my form
success: function(response)
{
let binaryString = window.atob(response);
let binaryLen = binaryString.length;
let bytes = new Uint8Array(binaryLen);
for (let i = 0; i < binaryLen; i++) {
let ascii = binaryString.charCodeAt(i);
bytes[i] = ascii;
}
let blob = new Blob([bytes], {type: "application/pdf"});
let link = URL.createObjectURL(blob);
window.open(link, '_blank');
},
error: function()
{
}
});
This will load the pdf in new window.
References: Return generated pdf using spring MVC

Related

Piping PDF to Express Response results in blank PDF

I'm trying to create a PDF in a Lambda function and I am having trouble using the .pipe() function from PDF Kit. When opening the downloaded PDF, it is blank. I have had success by converting the PDF to a base64 string and opening that, but this won't be feasible when the PDF size & # of requests increase.
My ideal approach is to use .pipe() as I've seen in multiple guides. This is the code below that is returning a blank PDF. I have tried using both responseType: 'blob' and responseType: arraybuffer on my client. Both open blank files.
let pdf = new PDFDocument();
pdf.text("hello world", 50, 50);
res.setHeader("Content-Type", "application/pdf");
res.setHeader(
"Content-Disposition",
`attachment; filename=test.pdf`
);
res.status(200);
pdf.pipe(res);
pdf.end();
This approach works, the PDF downloaded includes the "hello world" text, but this isn't a feasible approach due to performance/memory issues.
let chunks = [];
let pdf = new PDFDocument();
pdf.text("hello world", 50, 50);
pdf.on("data", data => {
chunks.push(data);
});
pdf.on("end", () => {
res.setHeader("Content-Type", "application/pdf");
res.setHeader(
"Content-Disposition",
`attachment; filename=test.pdf`
);
res.status(200);
const result = Buffer.concat(chunks);
res.send(
"data:application/pdf;base64," + result.toString("base64")
);
});
pdf.end();
I checked the contents of both PDF files (from both approaches above) and saw that the actual content is different between the two. This was also observed in the raw response logged in Chrome. I won't paste the entire file contents (unless someone thinks that's necessary), but here's where the files differ:
Blank PDF
stream
x�e�;
�0�=Ż���g� )-���*����7na'�c��pFǦ<yԛ�_[�d1�>�zӰ1�C�����ͻ��a��} .��d�J,pt�U���*
endstream
Working PDF
stream
xœeŒ;
€0û=Å»€šÍg£ )-ì„íÄ*ÎÂû7na'ÃcŠÇpFǦ<yÔ›â_[ô‹Œd1„>ŒzÓ°1ØC³Œ’¤Í»œØa––±«d³J,pt§Ué ÝÎ*
endstream
I don't know much about encoding, so not sure if this is helpful or just gibberish, but it does show there's something different in the encoding between the two files, and maybe I'm not setting something properly in Express.
Ended up using a workaround by converting the stream to base64 then decoding it in client.
Express function:
const { Base64Encode } = require("base64-stream");
let pdf = new PDFDocument();
pdf.text("hello world", 50, 50);
res.setHeader("Content-Type", "application/pdf");
res.setHeader(
"Content-Disposition",
`attachment; filename=test.pdf`
);
res.status(200);
pdf.pipe(new Base64Encode()).pipe(res);
pdf.end();
Client Code:
function base64ToArrayBuffer(data) {
var binaryString = window.atob(data);
var binaryLen = binaryString.length;
var bytes = new Uint8Array(binaryLen);
for (var i = 0; i < binaryLen; i++) {
var ascii = binaryString.charCodeAt(i);
bytes[i] = ascii;
}
return bytes;
}
// In my response handler:
const arraybuffer = base64ToArrayBuffer(response);
const blob = new Blob([arraybuffer], { type: "application/pdf" });
saveAs(blob, "test.pdf");

Angular 2 Image resize before upload

Im looking to resize an image before it is uploaded to a server, at the moment i am using ng2-imageupload like this:
<input id="media" class="inputfile" type="file" name="media" image-upload
(imageSelected)="selected($event)"
[resizeOptions]="resizeOptions" (change)="onChange($event)">
export class WpMediaFormComponent {
file: File;
resizeOptions: ResizeOptions = {
resizeMaxHeight: 768,
resizeMaxWidth: 438
};
selected(imageResult: ImageResult) {
console.log(imageResult);
this.dataBlob = this.dataURItoBlob(imageResult.resized.dataURL);
let blob = this.dataURItoBlob(imageResult.resized.dataURL);
}
This then returns an object, like this:
dataURL:"data:image/jpeg;base64, DATA URI HERE"
type:"image/jpeg;"
I can then convert this object to a blob using this function:
dataURItoBlob(dataURI) {
// convert base64/URLEncoded data component to raw binary data held in a string
var byteString;
if (dataURI.split(',')[0].indexOf('base64') >= 0)
byteString = atob(dataURI.split(',')[1]);
else
byteString = decodeURI(dataURI.split(',')[1]);
// separate out the mime component
var mimeString = dataURI.split(',')[0].split(':')[1].split(';')[0];
// write the bytes of the string to a typed array
var ia = new Uint8Array(byteString.length);
for (var i = 0; i < byteString.length; i++) {
ia[i] = byteString.charCodeAt(i);
}
return new Blob([ia], {type:mimeString});
}
Before doing this I was uploading the image to the server using this code:
onChange(event: EventTarget) {
let eventObj: MSInputMethodContext = <MSInputMethodContext> event;
let target: HTMLInputElement = <HTMLInputElement> eventObj.target;
let files: FileList = target.files;
this.file = files[0];
console.log(this.file);
//this.update.emit(this.file);
}
Does anyone have idea how I can feed the blob returned from dataURItoBlob method into the file upload onChange event?
Im a little lost here.
So I figured it out with the help of #Brother Woodrow, and this thread:
How to convert Blob to File in JavaScript
Here is my updated code, not the only thing I had to change was the selected method:
selected(imageResult: ImageResult) {
// create a blob
let blob: Blob = this.dataURItoBlob(imageResult.resized.dataURL);
// get the filename
let fileName: string = imageResult.file.name;
// create a file
this.file = new File([blob], fileName);
console.log(this.file);
// event emitter send to container then to http post
this.update.emit(this.file);
}
I can now upload 3MB and they are pushed to the server around 150kB in seconds which is great for the user especially as this app will mostly be used by mobile devices.
You'll need to convert the Data URI to a Blob, then send that back to your server. This might be helpful: Convert Data URI to File then append to FormData
Once you have the blob, it should be easy enough to use FormData and the Angular HTTP class to upload it to your server for further processing.
let formData = new FormData();
formData.append(blob);
this.http.post('your/api/url', fd).subscribe((response) => console.log(reponse);

Opening PDF served from servlet

Trying to send a PDF from a servlet to the client. The client sends a POST request through AJAX, and the request is handled by the servlet to generate a PDF and sends the PDF as a response to the client.
I've tried the options posted here to no avail (getting empty/un-openable pdfs): Opening PDF String in new window with javascript
Any help would be apprecieated!
So far, I can only get a formatted PDF String printing in the browser console using this code:
Java Servlet:
response.setContentType("application/pdf");
response.setHeader("Content-disposition","attachment; filename=ProgressReport.pdf");
response.setContentLength((int) pdfFile.length());
OutputStream out = response.getOutputStream();
FileInputStream in = new FileInputStream(pdfFile);
byte[] buffer = new byte[1024];
int length =0;
while ((length = in.read(buffer)) != -1){
out.write(buffer, 0, length);
System.out.println(buffer);
}
in.close();
out.flush();
JS
$.ajax({
url : "GenerateReport",
data : {
...
},
type : "POST",
success : function(result) {
console.log(result);
//window.open("data:application/pdf, " + encodeURI(result));
//download(result);
},
error : function(result) {
...
}
})
PDF String in Browser Console
%PDF-1.4 %����3 0 obj<</Filter/FlateDecode/Length 238>>streamx��QMO�#��x��(��D��!A�x�R��T�-�n��{�5LDB�e2�y�>2�l�Y$1�:a�i.�"�~f ...
I'm not 100% but your window.open is not the best as pop up blockers might prevent it as it's not a user action calling it's an AJAX response.
The better way would be the method outlined in this answer
var hiddenElement = document.createElement('a');
hiddenElement.href = 'data:attachment/text,' + encodeURI(result);
hiddenElement.target = '_blank';
hiddenElement.download = 'myFile.txt';
hiddenElement.click();
The other option is to use Base64 encoding and use "data:image/png;base64,"+result and in your C# you would need to create the buffer the size of the file and then base64 encode the entire buffer

Download File from Bytes in JavaScript

I want to download the file which is coming in the form of bytes from the AJAX response.
I tried to do it this way with the help of Blob:
var blob=new Blob([resultByte], {type: "application/pdf"});
var link=document.createElement('a');
link.href=window.URL.createObjectURL(blob);
link.download="myFileName.pdf";
link.click();
It is in fact downloading the pdf file but the file itself is corrupted.
How can I accomplish this?
I asked the question long time ago, so I might be wrong in some details.
It turns out that Blob needs array buffers. That's why base64 bytes need to be converted to array buffers first.
Here is the function to do that:
function base64ToArrayBuffer(base64) {
var binaryString = window.atob(base64);
var binaryLen = binaryString.length;
var bytes = new Uint8Array(binaryLen);
for (var i = 0; i < binaryLen; i++) {
var ascii = binaryString.charCodeAt(i);
bytes[i] = ascii;
}
return bytes;
}
Here is my function to save a pdf file:
function saveByteArray(reportName, byte) {
var blob = new Blob([byte], {type: "application/pdf"});
var link = document.createElement('a');
link.href = window.URL.createObjectURL(blob);
var fileName = reportName;
link.download = fileName;
link.click();
};
Here is how to use these two functions together:
var sampleArr = base64ToArrayBuffer(data);
saveByteArray("Sample Report", sampleArr);
You just need to add one extra line and it should work. Your response is byte array from your server application
var bytes = new Uint8Array(resultByte); // pass your byte response to this constructor
var blob=new Blob([bytes], {type: "application/pdf"});// change resultByte to bytes
var link=document.createElement('a');
link.href=window.URL.createObjectURL(blob);
link.download="myFileName.pdf";
link.click();
Set Blob type at Blob constructor instead of at createObjectURL
var blob = new Blob([resultByte], {type: "application/pdf"});
var link = document.createElement("a");
link.href = window.URL.createObjectURL(blob);
link.download = "myFileName.pdf";
link.click();
Easiest way would be converting bytes to the base64 format and construct link as below
let link=document.createElement('a');
const mimeType = "application/pdf";
link.href=`data:${mimeType};base64,${base64Str}`;
link.download="myFileName.pdf";
link.click();
Link can be generated on backend side and retrieved from the response.
File bytes can be read as base64 string in Python as following:
with open("my-file.pdf", "rb") as file:
base46_str = base64.b64encode(file.read()).decode("utf-8")

Pass PNG on form submit, Request URL Too long

So I have an interesting question. I have a form where a user draws an image on a canvas (think a signature pad). I then need to send the image to my C# Controller (I am using ASP.NET MVC 5). The code I have functions for shorter strings, but when I try to pass the PNG data, it is too long and I recieve a HTTP Error 414. The request URL is too long error. Here is my code:
Html:
<form id="mainForm" action="submitUserAnswer" method="post">
<input type="hidden" id="userOutput" name="output" value="" />
//...other form elements, signature box, etc.
</form>
Javascript:
function goToNextQuestion() {
var output = $('#signature').jSignature("getData");
$('#userOutput').val(output);
$('#mainForm').submit();
}
C#:
public ActionResult submitUserAnswer()
{
//use the userOutput for whatever
//submit string to the database, do trigger stuff, whatever
//go to next question
System.Collections.Specialized.NameValueCollection nvc = Request.Form;
string userOutput = nvc["output"];
ViewBag.Question = userOutput;
return RedirectToAction("redirectToIndex", new { input = userOutput });
}
public ActionResult redirectToIndex(String input)
{
ViewBag.Answer = input;
return View("Index");
}
My png data is very long, so the error makes sense. My question is how can I get the png data back to my controller?
Maybe you just need to increase allowed GET request URL length.
If that doesn't works I have an aspx WebForm that saves a signature, and I use a WebMethod
[ScriptMethod, WebMethod]
public static string saveSignature(string data)
{
/*..Code..*/
}
and I call it like this:
PageMethods.saveSignature(document.getElementById('canvas').toDataURL(), onSucess, onError);
also I have to increase the length of the JSON request and it works fine, with no problems with the lenght.
In MVC there isn't WebMethods, but JSON and AJAX requests do the job, just save the data in a session variable, and then use it when need it.
Hope it helps
You have error because your data is string (base64) and have max limit for send characters, better way is to create blob (png file) from base64 at client side, and send it to server. Edit. All listed code here, exists in stackoverflow posts.
function dataURItoBlob(dataURI) {
// convert base64 to raw binary data held in a string
// doesn't handle URLEncoded DataURIs - see SO answer #6850276 for code that does this
var byteString = atob(dataURI.split(',')[1]);
// separate out the mime component
var mimeString = dataURI.split(',')[0].split(':')[1].split(';')[0]
// write the bytes of the string to an ArrayBuffer
var ab = new ArrayBuffer(byteString.length);
var ia = new Uint8Array(ab);
for (var i = 0; i < byteString.length; i++) {
ia[i] = byteString.charCodeAt(i);
}
var blob = null;
// TypeError old chrome and FF
window.BlobBuilder = window.BlobBuilder ||
window.WebKitBlobBuilder ||
window.MozBlobBuilder ||
window.MSBlobBuilder;
if(window.BlobBuilder){
var bb = new BlobBuilder();
bb.append(ab);
blob = bb.getBlob(mimeString);
}else{
blob = new Blob([ab], {type : mimeString});
}
return blob;
}
function sendFileToServer(file, url, onFileSendComplete){
var formData = new FormData()
formData.append("file",file);
var xhr = new XMLHttpRequest();
xhr.open('POST', url, true);
xhr.onload = onFileSendComplete;
xhr.send(formData);
}
var base64 = $('#signature').jSignature("getData");
var blob = dataURItoBlob(base64);
var onComplete = function(){alert("file loaded to server");}
sendFileToServer(blob, "/server", onComplete)

Categories