27 juin 2015

PDF your collections, client side

Introduction

There are many ways to produce PDF on a website. You can use server side solutions for creating PDF, create CSS styles for print (and cross your finger waiting that all browser vendors will implement it) or create PDF on the client side. The two last solutions having the advantage not to take too much CPU on your infrastructure.

Leveraging PDFKit and SimpleSchema

PDFKit provides an almost complete isomorphic solution for that. It allows you to choose where you want to implement your PDF creation. Very nice. Unfortunately (or not), it only comes with low drawing primitives and for creating your PDF you will need some serious work on it.

When creating templates and forms for our collections, most of us are relying on Autoform. This solution is so nice that we tend to forget how incredible the amount of work this solution is doing for us. We just create a SimpleSchema of our collection and now we can display them, fill them, check the integrity of data provided by our users, and so forth. An incredibly productive package suite.

What if we could do the same for our PDF? That's what I was needing for a client. A solution that could take collections and create PDF out of them.

PDF Renderer

I've outsourced this solution in a package called pierreeric:pdfrenderer. Let's see a simple example on how to use it.

First, we start creating our classic collection with a SimpleSchema. We tag the fields that we want to see in our PDF using a simple pdf: true attribute:
CustomerSchema = new SimpleSchema
  name:
    type: String
    label: TAPi18n.__ 'name'
  images:
    type: String
    label: TAPi18n.__ 'images'
    optional: true
    autoform: afFieldInput:
      type: 'fileUpload'
      collection: 'Images'
  address:
    type: Object
    label: TAPi18n.__ 'address'
  'address.street':
    type: String
    label: TAPi18n.__ 'street'
    pdf: true
  'address.city':
    type: String
    label: TAPi18n.__ 'city'
    pdf: true

Customers = new Mongo.Collection 'customers'
Customers.attachSchema CustomerSchema

if Meteor.isServer
  if Customers.find().count() is 0
    Customers.insert
      name: 'Mathilde Charpentier'
      address:
        street: '227, rue Camille de Richelieu'
        city: 'Strasbourg'
  Meteor.publish 'customers', -> Customers.find()

if Meteor.isClient
  Template.svgTest.onCreated ->
    sub = @subscribe 'customers'
    @autorun =>
      if sub.ready()
        @customer = Customers.findOne()
  Template.svgTest.helpers
    customer: -> Template.instance().customer
Now for creating the PDF when the user click on a button, we can pass to the PdfRenderer some fields or the complete collection:
if Meteor.isClient
  Template.svgTest.events
    'click button': (e, t) ->
      # Create the initial PDF document
      pdf = new PdfRenderer size: 'a4'
      # Load all required assets
      pdf.addAsset "/cfs/files/images/#{t.customer.images}" if t.customer.images
      # Use reactivity for loading assets if any
      t.autorun ->
        if pdf.ready()
          # Customer image if exists
          if t.customer.images?
            pdf.img "/cfs/files/images/#{t.customer.images}", 'RIGHT',
              width: 100
          # Customer's name
          pdf.h1 t.customer.name
          # Address of customer
          pdf.h2 TAPi18n.__ 'address'
          pdf.schema CustomerSchema, 'address', t.customer
          # End the PDF document, display it and enable back the PDF button
          pdf.finish "file-#{t.customer.name}.pdf", ->
            console.log 'PDF finished'
You can see a rendered PDF in the Github repository.

Conclusion

This example is pretty simple. It could be enhance with theming and templating. Coupled with a CMS like Orion, this could be an interesting block for covering e-commerce solution with automatic catalogue creation, web magazines with press capabilities, bookkeeping with automatic invoices generation, ... Hope you will like it and share your contributions on this little package. Happy coding.