clamsy

Ruby wrapper for generating a single pdf for multiple contexts from an odt template.

1. Basics

A template odt is just an ordinary odt (en.wikipedia.org/wiki/OpenDocument). In order for it to function as a template, we just need to decorate it (just use openoffice, pls don’t buy ms office for that) with placeholders & embedded ruby code.

Under the hood, clamsy is using a slightly hacked version of the awesome tenjin template engine (www.kuwata-lab.com/tenjin) to do the rendering. The following is all u need to know to write odt templates:

  • ‘… ?’ represents embedded Ruby statement (this differs from vanilla tenjin)

  • ‘${ … }’ represents embedded Ruby expression which is to be escaped (eg. ‘& < > “’ are escaped to ‘&amp; &lt; &gt; &quot;’)

  • ‘#{ … }’ represents embedded Ruby expression (u should really avoid using this)

2. Examples

2.1. Text Replacement Example

Template Odt:

--------------------------------------------
 ${@company_full_name}
 ${@company_address_line_1}
 ${@company_address_line_2}

 S/N  Item Description       Amount (SGD)
 {? @items.each_with_index do |item, i| ?}
 ${i+1} ${item.description}  ${amount}
 {? end ?}
                      Total  ${total}
----------------------------------- Page 1 -

Code:

class Item
  attr_reader :description, :amount
  def initialize(description, amount)
    @description, @amount = description, amount
  end
end

items = [
  Item.new('Shells (x2 Bags)', 10),
  Item.new('Clams (x2 Bags)', 20)
]

context = {
  :company_full_name => 'Monster Inc',
  :company_address_line_1 => 'Planet Mars, Street xyz,',
  :company_address_line_2 => 'Postal Code 009900',
  :items => items,
  :total => items.inject(0) {|sum, item| sum + item.amount }
}

Clamsy.process(
  context,
  path_to_template_odt, # eg. '/tmp/invoice.odt'
  path_to_final_pdf,    # eg. '/tmp/invoice.pdf'
)

Generated Pdf:

--------------------------------------------
 Monster Inc
 Planet Mars, Street xyz,
 Postal Code 009900

 S/N  Item Description       Amount (SGD)

 1    Shells (x2 Bags)       10
 2    Clams (x2 Bags)        20

                      Total  30
----------------------------------- Page 1 -

2.2: Multiple Contexts Example

Taking the above example one step further, if we have multiple contexts:

items << Item.new('Shrimps (x2 Bags)', 15)
items << Item.new('Prawns (x2 Bags)', 25)

contexts = [{
  :company_full_name => 'Monster Inc',
  :company_address_line_1 => 'Planet Mars, Street xyz,',
  :company_address_line_2 => 'Postal Code 009900',
  :items => items[0..1],
  :total => items[0..1].inject(0) {|sum, item| sum + item.amount }
}, {
  :company_full_name => 'Fisherman Inc',
  :company_address_line_1 => 'Planet Jupiter, Street xyz,',
  :company_address_line_2 => 'Postal Code 008800',
  :items => items[2..3],
  :total => items[2..3].inject(0) {|sum, item| sum + item.amount }
}]

Clamsy.process(
  contexts,
  path_to_template_odt, # eg. '/tmp/invoice.odt'
  path_to_final_pdf     # eg. '/tmp/invoice.pdf'
)

Generated Pdf:

--------------------------------------------
 Monster Inc
 Planet Mars, Street xyz,
 Postal Code 009900

 S/N  Item Description       Amount (SGD)

 1    Shells (x2 Bags)       10
 2    Clams (x2 Bags)        20

                      Total  30
----------------------------------- Page 1 -
--------------------------------------------
 Fisherman Inc
 Planet Jupiter, Street xyz,
 Postal Code 008800

 S/N  Item Description       Amount (SGD)

 1    Shrimps (x2 Bags)      15
 2    Prawns (x2 Bags)       25

                      Total  40
----------------------------------- Page 1 -

As you can see, (whether you like it or not) the pages are merely concatenated together to form a single pdf !!

2.3: Image Replacement Example

It is possible to replace images in an odt file, a useful feature if u need to insert digital signature into the final pdf. To acheive this, u have to:

  • insert a placeholder image into the odt file,

  • resize & move it to your heart content

  • right click on the picture, under [Picture …]/[Options], assign a unique value for [Name]

Template Odt:

--------------------------------------------
      ~~~~~~~~~~~~~~~~~~
  Hey | nice_guy_image |
      ~~~~~~~~~~~~~~~~~~

  This is a gentle reminder that i still
    owe you some money ...

  ~~~~~~~~~~~~~~~~~~~~~~
  | my_signature_image |
  ~~~~~~~~~~~~~~~~~~~~~~
----------------------------------- Page 1 -

Assuming ‘nice_guy_image’ & ‘my_signature_image’ as names of the images, the following code does the appropriate replacing of images:

Clamsy.process(
  context = {:_pictures => {
    :nice_guy_image => path_to_avatar_image,       # eg. '/tmp/handsome.png'
    :my_signature_image => path_to_signature_image # eg. '/tmp/signature.png'
  }},
  path_to_template_odt # eg. '/tmp/confession.odt'
  path_to_final_pdf    # eg. '/tmp/confession.pdf'
)

3. Configuration

(see wiki.github.com/ngty/clamsy/configuration)

4. Pre-requisites

  • Make sure openoffice, java & ghostscript are installed

TODO

  • address security risk of user running destructive commands within embedded ruby in odt template

  • support for other platforms (eg. windows & jruby)

  • turn off non-santized ruby expression by default (since the generated doc easily breaks when underlying xml is invalid)

  • automate cups-pdf printer setup

Note on Patches/Pull Requests

  • Fork the project.

  • Make your feature addition or bug fix.

  • Add tests for it. This is important so I don’t break it in a future version unintentionally.

  • Commit, do not mess with rakefile, version, or history. (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)

  • Send me a pull request. Bonus points for topic branches.

Contacts

Written 2010 by:

  1. NgTzeYang, contact github.com/ngty

  2. JasonOng, contact github.com/jasonong

  3. ZhenyiTan, contact github.com/zhenyi

Released under the MIT license