Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

PDF thumbnail generation for Firebase

I am developing an Android app that manages PDFs. I know that Cloudinary allows users to upload PDFs and automatically generate thumbnails for the uploaded PDF (see here). Does Firebase Storage or Cloud Firestore offer a similar feature? If not, any recommended third-party tool for this task? Thanks!

like image 401
to_sam Avatar asked Jan 28 '23 05:01

to_sam


1 Answers

Intro

ImageMagick comes pre installed on the cloud function environment, but Ghostscript does not (at least, not right now). Ghostscript is what’s needed to generate images from PDFs.

First, download a copy of the Ghostscript executable from here (linux 64 bit APGL Release), and store it in the root of your functions directory. You may want to rename the folder/executable name for shorter path references. This will be deployed when you deploy your function(s).

Second as seen in this repo, npm install —-save https://github.com/sina-masnadi/node-gs/tarball/master . This is a wrapper you’ll need to communicate with the Ghostscript executable from within your function.

Third, you may want to look over the Ghostscript / ImageMagick docs to customize even further.

Sample cloud function

Finally, here is a function that I just got working for my functions file structure. You’ll need to write better validation and tweek it for your setup, but know that the meat of this will work.

const admin = require('firebase-admin');
const functions = require('firebase-functions');
const fs = require('fs');
const os = require('os');
const path = require('path');
const write = require('fs-writefile-promise');
const spawn = require('child-process-promise').spawn;
const mkdirp = require('mkdirp-promise');
const gs = require('gs');
const gs_exec_path = path.join(__dirname, '../../../ghostscript/./gs-923-linux-x86_64');

try { admin.initializeApp(functions.config().firebase); } catch(e) {}


/*
  Callable https function that takes a base 64 string of a pdf (MAX 
  10mb), uploads a thumbnail of it to firebase storage, and returns its 
  download url.
 */
module.exports = functions.https.onCall((data, context) => {
  if (!data.b64str) { throw Error('missing base 64 string of pdf!'); }

  const b64pdf = data.b64str.split(';base64,').pop();
  const pg = typeof data.pg === 'number' ? data.pg : 1; //1-based
  const max_wd = typeof data.max_wd === 'number' ? data.max_wd : 200;
  const max_ht = typeof data.max_ht === 'number' ? data.max_ht : 200;
  const st_fname = typeof data.fname === 'string' ? data.fname : 'whatever.jpg';

  const bucket = admin.storage().bucket();
  const tmp_dir = os.tmpdir();
  const tmp_pdf = path.join(tmp_dir, 'tmp.pdf');
  const tmp_png = path.join(tmp_dir, 'doesntmatter.png');
  const tmp_thumb = path.join(tmp_dir, st_fname.split('/').pop();
  const st_thumb = st_fname;

  /* create tmp directory to write tmp files to... */
  return mkdirp(tmp_dir).then(() => {
    /* create a temp pdf of the base 64 pdf */
    return write(tmp_pdf, b64pdf, {encoding:'base64'});
  }).then(() => {
    /* let ghostscript make a png of a page in the pdf */
    return new Promise((resolve, reject) => {
      gs().batch().nopause()
          .option(`-dFirstPage=${pg}`)
          .option(`-dLastPage=${pg}`)
          .executablePath(gs_exec_path)
          .device('png16m')
          .output(tmp_png)
          .input(tmp_pdf)
          .exec(err => err ? reject(err) : resolve());
    });
  }).then(() => {
    /* make a thumbnail for the png generated by ghostscript via imagemagick */
    var args = [ tmp_png, '-thumbnail', `${max_wd}x${max_ht}>`, tmp_thumb ];
    return spawn('convert', args, {capture: ['stdout', 'stderr']});
  }).then(() => {
    /* upload tmp_thumb to storage. */
    return bucket.upload(tmp_thumb, { destination: st_thumb });
  }).then(() => {
    /* get storage url for the uploaded thumbnail */
    return bucket.file(st_thumb).getSignedUrl({
      action:'read',
      expires: '03-01-2500'
    });
  }).then(result => {
    /* clean up temp files and respond w/ download url */
    fs.unlinkSync(tmp_pdf);
    fs.unlinkSync(tmp_png);
    fs.unlinkSync(tmp_thumb);
    return result[0];
  });
});

Sample invocation from client

var fn = firebase.functions().httpsCallable('https_fn_name');

fn({
  b64str: 'base64 string for pdf here....',
  pg: 2, //optional, which page do u want a thumbnail of? 1 is the first.
  fname: 'path/to/file/in/storage.jpg', //optional but recommended, .png if u like
  max_wd: 300, // optional, max thumbnail width
  max_ht: 300  // optional, max thumbnail height
}).then(res => console.log(res));

If you cant generate a signed url...

You'll need to Add the role "Cloud Functions Service Agent" to whatever service account this function is using. You can Add the role to your service account in your cloud console.

How to add the role

like image 116
Vincent Avatar answered Feb 07 '23 17:02

Vincent