3

I want to create a PNG thumbnail from different files which are uploaded on a Google Storage bucket. For the moment, I am targeting images and PDFs. For images the functions works fine, but for PDFs I cannot make it work. The idea is to download the file from the bucket, do the job and then upload the new file (the PNG thumbnail) to the bucket.

So I am doing a check to see the type of the uploaded file and if the file is an image I am doing the conversion with the createImageFromImage function and if it's PDF, I am using createImageFromPDF.

Main function:

const gm = require('gm').subClass({imageMagick: true});
const fs = require('fs');
const path = require('path');
const {Storage} = require('@google-cloud/storage');
const storage = new Storage();
const im = require('imagemagick');

exports.generatePreviewImage = event => {
  const object = event.data || event; // Node 6: event.data === Node 8+: event

  const file = storage.bucket(object.bucket).file(object.name);
  const filePath = `gs://${object.bucket}/${object.name}`;

  // Ignore already-resized files (to prevent re-invoking this function)
  if (file.name.endsWith('-thumb.png')) {
    console.log(`The image ${file.name} is already resized.`);
    return;
  } else {
    console.log(`Analyzing ${file.name}.`);
    //  Check the file extension
    if(object.contentType.startsWith('image/')) {  //  It's an image
      console.log("This is an image!")
      return createImageFromImage(file);
    } else if (object.contentType === 'application/pdf') {  //  It's a PDF
      console.log("This is a PDF file!")
      return createImageFromPDF(file);
    } else {
      return;
    }
  }
};

createImageFromImage(file) - which works

function createImageFromImage(file) {
  const tempLocalPath = `/tmp/${path.parse(file.name).base}`;

  // Download file from bucket.
  return file
    .download({destination: tempLocalPath})
    .catch(err => {
      console.error('Failed to download file.', err);
      return Promise.reject(err);
    })
    .then(() => {
      console.log(
        `Image ${file.name} has been downloaded to ${tempLocalPath}.`
      );

      // Resize the image using ImageMagick.
      return new Promise((resolve, reject) => {
        gm(tempLocalPath)
          .resize(250)
          .setFormat('png')
          .write(tempLocalPath, (err, stdout) => {
            if (err) {
              console.error('Failed to resize the image.', err);
              reject(err);
            } else {
              resolve(stdout);
            }
          });
      });
    })
    .then(() => {
      console.log(`Image ${file.name} has been resized.`);

      //  Get the name of the file without the file extension and mark the result as resized, to avoid re-triggering this function.
      const newName = `${path.parse(file.name).name}-thumb.png`;

      // Upload the Blurred image back into the bucket.
      return file.bucket
        .upload(tempLocalPath, {destination: newName})
        .catch(err => {
          console.error('Failed to upload resized image.', err);
          return Promise.reject(err);
        });
    })
    .then(() => {
      console.log(`Resized image has been uploaded to ${file.name}.`);

      // Delete the temporary file.
      return new Promise((resolve, reject) => {
        fs.unlink(tempLocalPath, err => {
          if (err) {
            reject(err);
          } else {
            resolve();
          }
        });
      });
    });
}

createImageFromPDF(file) - which doesn't work

function createImageFromPDF(file) {
  const tempLocalPath = `/tmp/${path.parse(file.name).base}`;

  return file
    .download({destination: tempLocalPath}) // Download file from bucket.
    .catch(err => {
      console.error('Failed to download file.', err);
      return Promise.reject(err);
    })
    .then(() => { // Convert the file to PDF.
      console.log(`File ${file.name} has been downloaded to ${tempLocalPath}.`);

      return new Promise((resolve, reject) => {

        im.convert([tempLocalPath, '-resize', '250x250', `${path.parse(file.name).name}-thumb.png`], 
          function(err, stdout) {
            if (err) {
              reject(err);
            } else {
              resolve(stdout);
            }
          });
      });
    })
    .then(() => { //  Upload the new image to the bucket
      console.log(`File ${file.name} has been resized.`);

      //  Get the name of the file without the file extension and mark the result as resized, to avoid re-triggering this function.
      const newName = `${path.parse(file.name).name}-thumb.png`;

      // Upload the Blurred image back into the bucket.
      return file.bucket
        .upload(tempLocalPath, {destination: newName})
        .catch(err => {
          console.error('Failed to upload resized image.', err);
          return Promise.reject(err);
        });
    })
    .then(() => { // Delete the temporary file.
      console.log(`Resized image has been uploaded to ${file.name}.`);

      return new Promise((resolve, reject) => {
        fs.unlink(tempLocalPath, err => {
          if (err) {
            reject(err);
          } else {
            resolve();
          }
        });
      });
    });
}

I get an error from im.convert which says: Command failed: convert: no images defined 'test1-thumb.png' @ error/convert.c/ConvertImageCommand/3210. I am not sure if this is the right way to create a PNG thumbnail from a PDF file, I tried other solutions without success. Please advise what I am doing wrong. Thanks!

decebal
  • 1,151
  • 3
  • 18
  • 38
  • Not sure you should call `im` this way, why not use the method chaining like with `gm`? Documentation proposes: `im(tempLocalPath).resize('250x250').convert(stdout)`. – Stock Overflaw Jan 29 '19 at 12:07
  • @StockOverflaw - Sorry, but I cannot see your suggestion on the documentation. I am looking here: https://github.com/rsms/node-imagemagick – decebal Jan 29 '19 at 12:13
  • Ow I thought you were using [this one](https://github.com/publicclass/im)! – Stock Overflaw Jan 29 '19 at 12:14
  • @StockOverflaw - I tried also with the one you provided, but I got a different error. – decebal Jan 29 '19 at 17:25

1 Answers1

3

I just realized gm can handle ImageMagick, and you already do it (using .subClass({imageMagick: true})), so why bother with another wrapper?

Anyway, I just tried this:

const gm = require('gm').subClass({imageMagick: true});
const file = './test.pdf';
gm(file)
.resize(250, 250)
.setFormat('png')
.write(file, (err) => {
    if (err) console.log('FAILED', err);
    else console.log('SUCCESS');
});

It stated some "not authorized" error because PDF processing is originally disabled - see this - but after I've edited /etc/ImageMagick*/policy.xml as suggested, it worked perfectly.

Stock Overflaw
  • 3,203
  • 1
  • 13
  • 14
  • Thanks for your message! I tried this but I get a different error: `Error: Command failed: convert: no images defined 'png:/tmp/Linux.pdf' @ error/convert.c/ConvertImageCommand/3210. at ChildProcess.onExit (/user_code/node_modules/gm/lib/command.js:301:17)......` - Anyway, where can I fiund `/etc/ImageMagick*/policy.xml` on a Windows machine? And also, if the file is downloaded from the bucket to the user's machine, converted and then uploaded back, this means the issue must be fixed for every used, right? – decebal Jan 30 '19 at 09:11
  • This was part of the error message I had, but below I had also a "not authorized" blahblah. Anyway, this could be related if you haven't edited the policies... if these are equivalent on Windows, I don't know. About the file's location, knowing Windows programs tend to put all their files in their installation directory, but I suppose you already looked in there. So... run a search for `policy.xml` in `C:` (including hidden stuff like ProgramData or Users/you/AppData). – Stock Overflaw Jan 30 '19 at 11:50
  • If your whole program is to run on a user's computer, then I guess you already have thought of a deployment system to install ImageMagick along with your code. So you already need the user's root permissions (since IM should create this file and protect with admin rights). So you can programatically access this policy file and edit it at that time. If, however, you let the users install IM as a required third-party they need to install on their own, then I guess you'll have to ask the user one more step, although that's not pretty... – Stock Overflaw Jan 30 '19 at 11:55