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");
Related
I have implemented a solution that accepts a single file upload (image for profile) in a Django Rest Framework backend. This route api/people/id/upload_image. Only accepts a parameter with an image. and can be used via HTTP POST.
When uploading via a fileinput field in eg. Postman, the default Django API or via browser fetch() in my Vue.js application is no problem. So it seems as long as it is a default form-upload field it is doing its job.
But in my frond-end (vuejs 3) I am using an image-cropper. Users can upload an image and via the javascript cropper the image can be cropped. This is important for the UI because I need a square image. The cropper uses HTMLCanvasElement.toDataURL() as export format.
And what seemed to be not that difficult gets me stuck for days now. I just can't find a way to convert and POST the cropped image in such a way that is accepted by the upload_image API backend. I am using Fetch() for sending this POST call.
I am not a javascript expert so I get my knowledge via internet and I tried it in several ways; with first creating a BLOB from the dataURI, and by creating a File before feeding it to dataForm and send it as :body in Fetch()
The dataURI seems OK because I am also replacing the cropped image directly in the HTML. And that looks totally fine. The API is responding with an 'HTTP 200 OK'. But the old image is not being replaced.
So my assumption is that there is something wrong with the image send to the API, because via normal fileupload everything works fine. How should I convert this dataURI in a proper way so it can be send and accepted by the API endpoint. And how should the API call look like: headers, body..
this is my last attempt in converting and sending the cropped image: (dataURIimage is OK)
uploadPhoto(context, dataURIimage) {
const blob = dataURItoBlob(dataURIimage);
const resultFile = new File([blob], "picture", {
type: "image/png"
});
const formData = new FormData();
formData.append("image", resultFile);
const headerToken = "Token" + " " + this.getters.getToken;
const url =
"https://workserver-7e6s4.ondigitalocean.app/api/people/" +
this.getters.getProfiel.id +
"/upload_image/";
fetch(url, {
method: "POST",
headers: {
'Content-Type': 'multipart/form-data',
'Authorization': headerToken,
},
body: formData,
})
.then(function(response) {
console.log(response.status)
if (response.ok) {
return response.json();
}
})
.then(function(data) {
console.log(data.image);
})
.catch(function(error) {
alert(error + " " + ":ERROR");
});
},
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);
}
return new Blob([ab], { type: mimeString });
}
Martijn dekker
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
I have an issue with FileSaver.js, I can not download a PDF (or PNG or excel file) on Safari, but it works on any other web browser. I get the error in the console : 'Failed to load resource: The network connection was lost.'
What is weird is that, this PDF file doesn't get downloaded if Tomcat serves it, but if it is Apache that serves the file, the download works fine.
Here is a sample of code (I am working with angular 1.5.8):
$http.get(url, { responseType: 'arraybuffer' })
.success(function (response) {
var file = new Blob([response], {type: 'application/pdf'});
fileSaverService(file, filename);
});
I had a similar issue and I was using axios to make a call (it was post request in my case) to the download service. The code below worked for me:
axios.post(url, downloadRequest, {responseType:'blob'})
.then(response =>{
var filename = 'example.zip';
var blob = new Blob([response.data], {type:"application/octet-stream"});
saveAs(blob , filename);
})
.catch(error => {
console.error(error);
});
I had a issue with base64,Pdf was not able to open.
This i have resolved converting base64 to bin and than converted into Uint8Array(byteNumbers).
Check below snippet which has worked for me.
const byteCharacters = atob(b64);
const byteNumbers = new Array(byteCharacters.length);
for (let i = 0; i < byteCharacters.length; i++) {
byteNumbers[i] = byteCharacters.charCodeAt(i);
}
const byteArray = new Uint8Array(byteNumbers);
var blob = new Blob([byteArray], {type: "application/pdf"});
saveAs(blob, "sample.pdf");
I have an express server running with the following route:
exports.getUserFile = function (req, resp) {
let filePath = path.join(__dirname, 'storage', req.params.fileName);
resp.download(filePath);
});
}
In my web app i'm calling this route and trying to save the file locally using file-saver:
let req = request.get('/users/' + userId + '/files/' + file.name);
req.set('Authorization', 'Bearer ' + this.state.jsonWebToken);
req.end((err, resp) => {
let f = new File([resp.text], file.name, {type: resp.type});
fileSaver.saveAs(f);
});
If the file is plain text then it works ok, but for other file types like images i'm not able to open the file (it's 'corrupt').
This is what the response looks like:
Do I need to decode the data in some way first? What is the correct way to save the content of the file?
If you're using superagent to perform the requests, you can explicitly set the response type to "blob", which would prevent any attempts to decode the response data. The binary data will end up in resp.body:
req.responseType('blob').end((err, resp) => {
saveAs(resp.body, file.name);
});
I haven't used express for a long time ago and I'm typing from mobile, it's seems a encoding issue, so it's seems that you're a sending raw image, you will need to encode it in base64 try something like:
//Here your saved file needs to be encoded to base 64.
var img = new Buffer(data, 'base64');
res.writeHead(200, {
'Content-Type': 'image/png',
'Content-Length': img.length
});
res.end(img);
Where data is your saved image, If you can render the image you just add the headers for download or just chain method download.
If you want to download the image as attachment in the page you can use res
exports.getUserFile = function (req, resp) {
let filePath = path.join(__dirname, 'storage', req.params.fileName);
var check = fs.readFileSync(__dirname+req.params.fileName);
resp.attachment(req.params.fileName); // The name of the file to be saved as. Eg Picture.jpg
res.resp(check) // Image buffer read from the path.
});
}
Reference:
http://expressjs.com/en/api.html#res.attachment
http://expressjs.com/en/api.html#res.end
Hope this helps.
I send multiple files chunked into Blob's over XHR2 to a Node.js/Express server.
How can I receive them on the server while making sure they are put together correctly? In their right order and to the right file when multiple files are uploaded "at once".
Following is the code (both front- and backend) I have so far but doesn't account for multiple uploads yet.
Frontend:
// 'files' is of type FileList, directly from file input.
for (var i = 0, length = files.length; i < length; i++) {
var file = files[i];
var bytes = 51200; // 50 KB
var size = file.size;
var start = 0;
var end = bytes;
while (start < size) {
sendBlob(file.slice(start, end), file.name, file.type);
start = end;
end = start + bytes;
}
}
// sendBlob()
var sendBlob: function (data, filename, filetype) {
var xhr = new XMLHttpRequest();
xhr.open('POST', this.url, false);
xhr.setRequestHeader('X_FILENAME', filename);
xhr.setRequestHeader('Content-Type', filetype);
xhr.send(data);
};
Backend:
app.post('/', function (req, res) {
var body = '';
req.on('data', function (data) {
body += data;
});
req.on('end', function () {
var filename = req.headers['x_filename'];
var newPath = __dirname + '/upload/' + filename;
fs.writeFile(newPath, body, function (err) {
res.send({
filename: filename
});
});
});
});
Very small text files are stored correctly but images seem to always get messed up and end up with a bigger file size. Bigger text files are written correctly but there the first chunk seems to be missing.
Your upload logic is naive. Here are some things you should do to ensure correctness :
You have to maintain and communicate the chunk id/number between client and server so that order can be maintained.
var sendBlob: function (data, filename, filetype, chunkid)
//set chunkid in header or in data.
In your server you are accepting any post request and appending it to the body. You should maintain variables for filename and filetype and match it with incoming request before appending it.
Files[Name] = { //Create a new Entry in The Files Variable for each new file
Filetype : "",
FileSize: 0,//size of Data in buffer
Data: "", //buffer for storing data
Downloaded: //chunks recieved
}
Append to Data only when you check it. (Extra file size could be due to this)
In your fs.writeFile you should set encoding as binary, image and video files are binary encoded and writing them into default utf-8 encoding may corrupt them.
fs.writeFile(newPath, body, 'binary', function (err){...});
(optional) For each chunk received by server it should send an acknowledgement back to client so that it knows which chunk is dropped and must be sent.