= clamsy Ruby wrapper for generating a single pdf for multiple contexts from an odt template. clamsy = clumsy + shellish | | | |-- under the hood, we are making system calls to ooffice & | cups server needs to be running | |-- setup isn't straight forward, need to install a couple of packages (how bad it is depends on ur platform) & even manual setting up of cups-pdf printer is needed == 1. Basics A template odt is just an ordinary odt (http://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 (http://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 '& < > "') * '#{ ... }' 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 === 3.1. User-defined ~/.clamsy.yml printer: cups_pdf cups_pdf: cups_output_dir: /var/spool/cups-pdf/${USER} cups_output_file: ooffice_cmd: ooffice -norestore -nofirststartwizard -nologo -headless -pt Cups-PDF NOTES: * ${...} gets replaced by the corresponding ENV variable, eg. ${USER} gets replaced by ENV['USER'] * printer ... describes which printer to use (currently, we ONLY have 'cups_pdf') * cups_output_dir ... the directory where cups-pdf places the intermediate working pdf, in linux box, it is specifed by the 'Out' variable in /etc/cups/cups-pdf.conf * cups_output_file ... specifies the exact intermediate working pdf, when this is specified, cups_output_dir is ignored * ooffice_cmd ... the openoffice command & arguments to use === 3.2. Clamsy\.configure {|config| ... } Clamsy.configure do |config| config.printer = 'another_printer' config.config_file = '/path/to/alternative/clamsy.yml' config.cups_output_dir = '/another/output/dir' config.cups_output_file = lambda { Dir.glob('/yet/another/output/dir/*.pdf').sort.last } end NOTES: * config_file ... alternative user-defined config file, when specified, ~/.clamsy.yml will be ignored (whatever is supported in this config file is exactly the same as 3.1) * cups_output_file ... unlike what we have in the yaml config file, this can be either a proc or a string === 3.3. Clamsy\.process(...) {|config| ... } Clamsy.process(contexts, template_odt, final_pdf) do |config| # the rest is the same as 3.2 end == 4. Pre-requisites === 4.1. Archlinux * Installing packages: $ sudo pacman -S ghostscript cups cups-pdf go-openoffice * Setting up the cups-pdf virtual printer by following instructions @ http://wiki.archlinux.org/index.php/CUPS#Configuring_CUPS-PDF_virtual_printer * Making sure cups is running: $ sudo /etc/rc.d/cups start === 4.2. Ubuntu (to-be-updated) === 4.3. Mac (to-be-updated) == TODO * automate cups-pdf printer setup * more printers support (currently, we ONLY have Clamsy::CupsPdfPrinter) == 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 http://github.com/ngty 2. JasonOng, contact http://github.com/jasonong 3. ZhenyiTan, contact http://github.com/zhenyi Released under the MIT license