Search code examples
node.jsasynchronousnode-pdfkit

How use async functions with node-pdfkit?


I am trying to create a PDF file with PDFKit. I insert an image with like this:

var PDFDocument = require('pdfkit');
var doc = new PDFDocument();

doc.image(some_image_as_buffer);

and it is working like expected. But now want the image be trimmed and I found GraphicsMagick for node.js. But the problem that I have is to make it work with PDFKit. doc.image expects a filename or a buffer, but since I already have a buffer I want to work with buffers (there is no file anywhere because the buffer comes directly from the database).

The trimming works like this:

var gm = require('gm');
gm(some_image_as_buffer, 'image.png')
  .trim()
  .toBuffer(function(err, trimmed_image_buffer) {
    // trimmed_image_buffer is correct,
    // but I can't put it to the document like this:
    doc.image(trimmed_image_buffer);
    // beacause I don't know which page and/or position
    // the doc is currently on, because of the asynchronous
    // nature of this callback.
  });

UPDATE:

For clarification: I want to be able to use the asynchronous trimmed image in the synchronous code for PDFKit. PDFKit only works synchronously and gm doesn't offer a synchronous interface.

UPDATE2:

var gm = require('gm');
gm(some_image_as_buffer, 'image.png')
  .trim()
  .toBuffer(function(err, trimmed_image_buffer) {
    // trimmed_image_buffer is correct,
    // but I can't put it to the document like this:
    doc.image(trimmed_image_buffer);
    // beacause I don't know which page and/or position
    // the doc is currently on, because of the asynchronous
    // nature of this callback.
  });
doc.text('some text');
// is not guaranteed to run after image is inserted
// and a couple of hundred lines more

After the last line in this example there are a lot more lines of code which add content to the PDF, but I don't want to put everything (couple of hundred lines) in one callback just because I need on asynchronous function to manipulate the image.

Is there any way to make this manipulation synchronous?


Solution

  • UPDATE_2

    You basically ask for stopping execution of a code until some asynchronous operation has completed. For sure it is not possible in general case.

    In case of gm module, it is not possible either. The gm module spawns a new process for executing a command (in your case trim()) and the API for spawning new processes is asynchronous in its very nature.

    UPDATE

    To make use of promise in your scenario:

    var gm = require('gm'),
        Q = require('Q'),
        PDFDocument = require('pdfkit'),
        doc = new PDFDocument();
    
    function getTrimmedImage(some_image_as_buffer){
      var deferred = Q.defer(); 
      gm(some_image_as_buffer, 'image.png')
      .trim()
      .toBuffer(function(err, trimmed_image_buffer) {
        if(err) { deferred.reject(err); }
        else { deferred.resolve(trimmed_image_buffer); }
      });
      return deferred.promise;
    }
    
    // here goes all manipulations before the trimmed image is inserted
    
    getTrimmedImage(some_image_as_buffer).then(
      function(trimmed_image_buffer){
         doc.image(trimmed_image_buffer);
    
         // here goes all manipulations after the trimmed image is inserted
    
      }
    );
    

    As I wrote in the comment above, a promise based solution should work elegantly. I use Q library, but any other promise library will do the job, as well.

    One option would be to collect all resources of asynchronous nature before starting manipulating the pdf. Then you are guaranteed that no race condition occur, though it may slow down the whole process. I used a toy example to have it working in the browser environment, let me know if you have any problems converting it to your use case:

    function getAsyncResource(){
      
      var defer = Q.defer();
      
      setTimeout(function(){
        var result = "Some value: " + Date.now();
        console.log("Async resource resolved: " + result); 
        defer.resolve(result);
        }, Math.random() * 5000);
      
      
      return defer.promise;
      }
    
    function someOperationThatNeedsAsyncResources(A, B, C){
      
      console.log("Have all resources: ", A, B, C);
      
      }
    
    var A = getAsyncResource(),
        B = getAsyncResource(),
        C = getAsyncResource();
    
    Q.all([A,B,C]).spread(someOperationThatNeedsAsyncResources);
    <script src="//cdnjs.cloudflare.com/ajax/libs/q.js/1.1.2/q.js"></script>

    Other option would be to split the process into steps, like so:

    function getAsyncResource(value){
      
      var defer = Q.defer();
      
      setTimeout(function(){
        var result = "Some value: " + value;
        console.log("Async resource resolved: " + result); 
        defer.resolve(result);
        }, Math.random() * 5000);
      
      
      return defer.promise;
      }
    
    function nextStep(resource){
       console.log("Next step: " + resource);
      }
    
    var A = getAsyncResource("A"),
        B = getAsyncResource("B"),
        C = getAsyncResource("C");
    
    A.then(nextStep)
     .then(function(){return B;})
     .then(nextStep)
     .then(function(){return C;})
     .then(nextStep);
    <script src="//cdnjs.cloudflare.com/ajax/libs/q.js/1.1.2/q.js"></script>