16

I am new to javascript and I'm trying to make a PDF file from a firebase function using pdfkit. Below is my function code.

const pdfkit = require('pdfkit');
const fs = require('fs');

exports.PDFTest = functions.https.onRequest((req, res) => {

var doc = new pdfkit();

var loremIpsum = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam in...';  

doc.y = 320;
doc.fillColor('black')
doc.text(loremIpsum, {
paragraphGap: 10,
indent: 20,
align: 'justify',
columns: 2
});  

doc.pipe( res.status(200) )

});

The function starts but then a timeout error happens. Is this the best way of going about creating a pdf file in firebase? I have some html that I want made into a pdf file.

user1184205
  • 863
  • 1
  • 10
  • 23

5 Answers5

24

I worked on this too and here below you can find a sample of cloud function that is creating a PDF file from a template HTML hosted on firebase storage. It uses Hanldebars to apply some data to the template and then upload it again on firebase storage. I used node-html-pdf here.

const functions = require('firebase-functions');
const admin = require('firebase-admin');
const pdf = require('html-pdf');
const gcs = require('@google-cloud/storage')({
  projectId: '[YOUR PROJECT ID]',
  //key generated from here https://console.firebase.google.com/project/_/settings/serviceaccounts/adminsdk?authuser=1
  keyFilename: '[YOUR KEY]'
});
const handlebars = require('handlebars');
const path = require('path');
const os = require('os');
const fs = require('fs');
const bucket = gcs.bucket('[YOUR PROJECT ID].appspot.com');

admin.initializeApp(functions.config().firebase);

exports.helloWorld = functions.https.onRequest((request, response) => {
  // data to apply to template file
  const user = {
    "date": new Date().toISOString(),
    "firstname" : "Guillaume",
  };
  const options = {
    "format": 'A4',
    "orientation": "portrait"
  };
  const localTemplate = path.join(os.tmpdir(), 'localTemplate.html');
  const localPDFFile = path.join(os.tmpdir(), 'localPDFFile.pdf');

  bucket.file('template.html').download({ destination: localTemplate }).then(() => {
    console.log("template downloaded locally");
    const source = fs.readFileSync(localTemplate, 'utf8');
    const html = handlebars.compile(source)(user);
    console.log("template compiled with user data", html);

    pdf.create(html, options).toFile(localPDFFile, function(err, res) {
      if (err){
        console.log(err);
        return response.send("PDF creation error");
      }
      console.log("pdf created locally");

      return bucket.upload(localPDFFile, { destination: user.name + '.pdf', metadata: { contentType: 'application/pdf'}}).then(() => {
        response.send("PDF created and uploaded!");
      }).catch(error => {
        console.error(error);
        response.send("PDF created and uploaded!");
      });
  });
  });
});

Hope this will help the next one doing this :)

Guillaume Gendre
  • 2,504
  • 28
  • 17
  • Hi Guillaume! What contains exactly inside template.html? – Mario Jan 29 '18 at 23:12
  • 1
    @Mario You have to use an html template according to http://handlebarsjs.com/ documentation . In this case something like that is ok:
    {{date}} {{firstname}}

    However, after the template has been apparently correctly compiled, I get the following error... Error: html-pdf: PDF generation timeout. Phantom.js script did not exit.

    – Renaud Tarnec Feb 01 '18 at 09:58
  • there is a `timeout` settings on node-html-pdf. maybe it could help. The phantomjs used to generate the PDF is also a bit old, dont use `let` or new javascript stuff otherwise you will get a blank PDF or worst, this kind of error.. – Guillaume Gendre Jun 25 '18 at 08:06
  • What if you had multiple users using the app and generating PDF files? How do we know what PDF belongs to each user and how do we clean the temp files after uploading to Firebase Storage? –  Jan 24 '19 at 00:19
  • @GuillaumeGendre I am able to compile the template into html but `html-pdf` ignores the content inside my ` – Badrush Feb 14 '19 at 17:52
8

Just working on this too, for saving the PDF in the storage it works like this

const myPdfFile = admin.storage().bucket().file('/test/Arbeitsvertrag.pdf');
const doc = new pdfkit();
const stream = doc.pipe(myPdfFile.createWriteStream());
doc.fontSize(25).text('Test 4 PDF!', 100, 100);
doc.end();

return res.status(200).send();

Guess you should wait until the stream is closed and listen for Errors and Things, but this is the first working example I was able to make, now working on how to get a Image from the storage into the PDF.

Chris
  • 128
  • 2
  • 7
  • I ended up using html-pdf, it worked great for me. I will accept your answer. – user1184205 Oct 23 '17 at 02:03
  • 1
    @user1184205 Can you update your answer showing how you used html-pdf? I would like to also use something that translates html to a pdf inside firebase. – SamIAmHarris Dec 18 '18 at 00:21
4

I tried Guillaume's suggestion and it ALMOST got me there. Unfortunately, Phantomjs was exiting without finishing.

I ended up solving this by combining Guillaume's solution and https://phantomjscloud.com (and their library.) Now it's all working like a charm.

After "template compiled with user data", substitute the following:

 const phantomJsCloud = require("phantomjscloud");
 const browser = new phantomJsCloud.BrowserApi([YOURPHANTOMJSCLOUDAPIKEY]);

 var pageRequest = { content: html, renderType: "pdf" }; 

 // Send our HTML to PhantomJS to convert to PDF

 return browser.requestSingle(pageRequest)
      .then(function (userResponse) {
          if (userResponse.statusCode != 200) {
               console.log("invalid status code" + userResponse.statusCode);
            } else {
               console.log('Successfully generated PDF');

               // Save the PDF locally
               fs.writeFile(localPDFFile, userResponse.content.data, {
                           encoding: userResponse.content.encoding,
                       }, function (err) {                             
                           // Upload the file to our cloud bucket
                           return pdfBucket.upload(localPDFFile, { destination: 'desired-filename.pdf', metadata: { contentType: 'application/pdf'}}).then(() => {
                             console.log('bucket upload complete: '+ localPDFFile);
                           }).catch(error => {
                             console.error('bucket upload error:', error);
                           });
                       });

                   }

                   });
  • there was a timeout parameter in html-pdf to wait a bit more if you needed it. but phantomjscloud or cloudconvert are making a good job. – Guillaume Gendre Nov 06 '18 at 10:13
4

A little bit late.

https://edgecoders.com/generating-a-pdf-with-express-in-node-js-d3ff5107dff1


import * as functions from 'firebase-functions';
import * as PDFDocument from 'pdfkit';

export const yourFunction = functions
  .https
  .onRequest((req,  res) => {
    const doc = new PDFDocument();
    let filename = req.body.filename;
    // Stripping special characters
    filename = encodeURIComponent(filename) + '.pdf';
    // Setting response to 'attachment' (download).
    // If you use 'inline' here it will automatically open the PDF
    res.setHeader('Content-disposition', 'attachment; filename="' + filename + '"');
    res.setHeader('Content-type', 'application/pdf');
    const content = req.body.content;
    doc.y = 300;
    doc.text(content, 50, 50);
    doc.pipe(res);
    doc.end();
  });

hope this can help anyone

2

I ran into this question while searching for the same as the OP but in my particular case switching libraries wasn't an option and I was particularly interested in outputting the PDF directly.

After getting it to work successfully and comparing to what the OP was doing it seems that all that was missing was doc.end() to flush the data.

Here's PDFKit's demo being output by Firebase Function:

const PDFDocument = require('pdfkit');

exports.PDFTest = functions.https.onRequest((req, res) => {

    var doc = new PDFDocument();

    // draw some text
    doc.fontSize(25)
       .text('Here is some vector graphics...', 100, 80);

    // some vector graphics
    doc.save()
       .moveTo(100, 150)
       .lineTo(100, 250)
       .lineTo(200, 250)
       .fill("#FF3300");

    doc.circle(280, 200, 50)
       .fill("#6600FF");

    // an SVG path
    doc.scale(0.6)
       .translate(470, 130)
       .path('M 250,75 L 323,301 131,161 369,161 177,301 z')
       .fill('red', 'even-odd')
       .restore();

    // and some justified text wrapped into columns
    doc.text('And here is some wrapped text...', 100, 300)
       .font('Times-Roman', 13)
       .moveDown()
       .text("... lorem ipsum would go here...", {
         width: 412,
         align: 'justify',
         indent: 20,
         columns: 2,
         height: 300,
         ellipsis: true
       });


    doc.pipe(res.status(200));

    doc.end();

});

This is a very simple example and probably would need appropriate headers to be sent but it works as is in modern browsers. I hope this helps anyone looking for the same.

lbarbosa
  • 2,042
  • 20
  • 23