Generating PDF Documents with Rails and PDFlib - Part I

Posted by Bob Silva Sun, 02 Apr 2006 14:16:00 GMT


Generating PDF documents programmatically is probably one of the worse programming tasks I've had the pleasure to work on. It's not terribly difficult, it's just tedious and time consuming. Some of my applications need to print catalogs or reports upwards of 300+ pages in real-time, so speed is of the essence, unfortunately, the pure Ruby PDF::Writer isn't up to the job. PDFlib-Lite, (read the license before using), is a fast library, which in the lastest 6.0.3 release contains Ruby Bindings (albeit horribly broken).

Installing PDFlib-Lite


Installing PDFlib is similar on Linux and MacOSX.

Download the lastest tarball

Extract the source

src> tar zxf PDFlib-Lite-6.0.3.tar.gz
src> cd PDFlib-Lite-6.0.3


If using 6.0.3, download this patch to the root of your extracted PDFlib-Lite source, apply the patch, and regenerate the configure script

PDFlib-Lite-6.0.3> patch -p0 < configure.in.diff
PDFlib-Lite-6.0.3> autoconf


Run the configure script. Use --help for additional options/language bindings. Your paths may be different, if you can't figure it out, switch to Windows.

On linux:
PDFlib-Lite-6.0.3> ./configure --with-ruby=/usr/bin/ruby --with-rubyincl=/usr/lib/ruby/1.8/i686-linux


On MacOSX (you built your own Ruby right?):
PDFlib-Lite-6.0.3> ./configure --with-ruby=/usr/local/bin/ruby --with-rubyincl=/usr/local/lib/ruby/1.8/i686-darwin8.5.2


If you followed these instructions, the configure script should tell you that the Ruby bindings are active.

Ruby language binding for PDFlib: yes


Build the library, (the 'make test' is required for these instructions to work)

PDFlib-Lite-6.0.3> make
PDFlib-Lite-6.0.3> make test
PDFlib-Lite-6.0.3> make install


All right, so now you'd think it would work since you've installed it. Remember when I said the Ruby bindings were broken, besides the patch to get PDFlib to realize it has Ruby bindings, you also have to manually copy the library to your Ruby's site_ruby/ directory. Optionally, you can symlink the real library found in your /usr/lib or /usr/local/lib directory.

PDFlib-Lite-6.0.3> cd bind/pdflib/ruby

On linux:
PDFlib-Lite-6.0.3/bind/pdflib/ruby> cp PDFlib.so /usr/lib/ruby/site_ruby/1.8


On MacOSX:
PDFlib-Lite-6.0.3/bind/pdflib/ruby> cp PDFlib.bundle /usr/local/lib/ruby/site_ruby/1.8



We'll cover usage of PDFlib in part II of this article which should be complete in a week or so.

Implement acts_as_threaded without a plugin 30

Posted by Bob Silva Fri, 31 Mar 2006 20:40:00 GMT

It's been awhile since I've posted so I thought I'd toss this out there. If you've used my acts_as_threaded plugin, you know its an off-shoot from the acts_as_nested set inside of AR itself. I've managed to create the same functionality without the use of a plugin using the native acts_as_nested_set feature of AR. The relevant code is below. The threading functionality has been moved to the model now, add the following code to your model and you are set.
  acts_as_nested_set :scope => :root
    
  def before_create
    # Update the child object with its parents attrs
    unless self[:parent_id].to_i.zero?
      self[:depth] = parent[:depth].to_i + 1
      self[:root_id] = parent[:root_id].to_i
    end
  end
  
  def after_create
    # Update the parent root_id with its id
    if self[:parent_id].to_i.zero?
      self[:root_id] = self[:id]
      self.save
    else
      parent.add_child self
    end
  end
  
  def parent
    @parent ||= self.class.find(self[:parent_id])
  end

Your database schema will need to have the following definition:
create_table "my_table_name", :force => true do |t|
  t.column "root_id", :integer
  t.column "parent_id", :integer
  t.column "lft", :integer
  t.column "rgt", :integer
  t.column "depth", :integer
end
The trick from this point, is that whenever you create a new thread, if it has a parent_id, then it will automatically be added as a child to that parent record. Otherwise, it will be set as a root thread. This version no longer requires that the fields have a default value of 0 relying on the fact that 'NilClass.to_i == 0'.

Hope you enjoy this, it's come in very handy for modeling structured content in some of my apps (like categories and multi-level organizations).

My Macbook Finally Arrived

Posted by Bob Silva Sun, 05 Mar 2006 09:06:00 GMT

One word, two meanings. It's HOT! and Ah crap, it is HOT!

Being my first experience with Mac, I am very pleased with it. I'm still coming up to speed with OSX and haven't had much time to play with it yet. The crowning moment came when I hooked it up to the 30" cinema. What a beautiful thing to have 6 full size browser windows visible on your desktop. Pic #1 Pic #2

I am a little disappointed in the amount of heat it generates though. I made the mistake of setting it on my lap after running for a few hours, and even though I had shorts on, it still got my attention rather quick. The little area just above the F keys gets a bit toasty as well. Since the majority of the heat comes from the battery, I am a little concerned that there may be other issues than just "Apple laptops have always run hot". But who knows? For now, I'm just wanting to finish up my current projects so I can have some time to play with it more.

More Advanced Rails Schema Generation 1

Posted by Bob Silva Sun, 05 Mar 2006 08:17:00 GMT

One of the problems I've seen with Rails as of late is that the database schemas for the unit tests get out of sync with each other. As new features get added, some db_definitions get updated and others missed. The core group is well aware of this issue and DHH left a challenge in a ticket I created to add missing table definitions from a previous changeset.


I decided I would take him up on his challenge and went to work. I printed off all the db schemas and started comparing them with each other, highlighting where they differed. This was my first indication as to the scope of this conversion.


Since the purpose of converting over to using schema.rb is to maintain one schema for all the tests, it would be helpful if all the existing definitions were at least similar to begin with. Most of the differences can be fixed without issue. Converting a varchar(3000) field to text, or a timestamp to datetime is no big deal as far as the unittests are concerned. The biggest road block is that some tables are defined using features that don't exist in AR::Schema. Features like foreign keys, arbitrary primary keys and sequences. User-defined column definitions is yet another example where AR::Schema isn't up to the job. For example, the Postgres tests use a table with geometric field types.


I had a pretty good idea how to implement everything except for sequences, so I started coding and within a few hours had the test suite working with MySQL using schema.rb. Before I dove much further into making all the adapters work with schema.rb, I decided I better get some feedback on whether these changes I was making to Rails schema generation would even be accepted into core. Unfortunately, I didn't get any feedback from the core developers.


I think the biggest issue they would raise is whether or not the SchemaDumper would be able dump these changes. The short answer is no, which coincidently, is probably the same answer my patch would get. So I scrapped my work on converting the unittests over but will consider making a plugin which adds this functionality (and much more) for more advanced schema generation/migrations. You just won't be able to dump your schema out into a schema.rb file.


Let me know if you think this would be useful and I'll put something together.

Handling Invalid Dates with ActiveRecord Date Helpers 3

Posted by Bob Silva Thu, 23 Feb 2006 03:42:00 GMT

If you've worked with the model based date helpers in Rails (date_select and datetime_select), then you may know that selecting an invalid date throws an exception deep within ActiveRecord. A lot of users assume they can handle this in the model validation, but due to the way ActiveRecord handles multi-parameter input values, it will raise an exception if an invalid date is input. Here's some sample code of how you can catch and handle this exception without affecting the user experience too much.
In controller.rb:

def create
  begin
    @client = Client.new(params[:client])
  # Catch the exception from AR::Base
  rescue ActiveRecord::MultiparameterAssignmentErrors => ex
    # Iterarate over the exceptions and remove the invalid field components from the input
    ex.errors.each { |err| params[:client].delete_if { |key, value| key =~ /^#{err.attribute}/ } }
    # Recreate the Model with the bad input fields removed
    @client = Client.new(params[:client])      
  end
    
  if @client.save
    flash[:notice] = 'Client was successfully created.'
    redirect_to :action => 'list'
  else
    render :action => 'new'
  end
end

In model.rb:

validates_presence_of :date_field_name, :message => 'must be a valid date'

Extending script.aculo.us 1

Posted by Bob Silva Thu, 16 Feb 2006 10:23:00 GMT

One of the great features of Rails is its plugin system. It allows you to add new functionality to the framework without modifying the original source. On my current project, I had a need to create a CachingAutoCompleter and I wanted to do it within Rails.


Rails has a method that will work just fine, text_field_with_auto_complete. The problem lies in the fact that this method creates an Ajax.Autocompleter, whereas I need an AutoComplete object with my caching functionality. I could modify the script.aculo.us source for Ajax.Autocompletor and add my caching layer but surely there's gotta be a better way, something akin to the Rails plugin system.


Click the image below to watch a screencast (7MB) of how I accomplished this goal.



You can download the source of the CachingAutoCompleter here.

Rebuilt the Date Helper Library 1

Posted by Bob Silva Sun, 12 Feb 2006 16:39:00 GMT

So I locked myself in my office Saturday and decided to rebuild the sorely lacking date_helper.rb that comes with Rails.


It was quite the daunting task for there were no less than 12 existing tickets for bug fix/enhancements. 17 hours later, a new date_helper was born that resolves all the issues and its quite DRY as well. Now we just gotta hope the developers get a chance to look at it sometime. Some of the fixes it contains have been around for ages so I'm assuming the core members don't use dates very often in their applications.


Heres a CHANGELOG:

  • Makes :discard_year work without breaking multi-attribute parsing in AR. #1260 #3800
  • Adds html id attribute to each element. #1050 #1382
  • Adds :index and @auto_index capability to model driven date/time selects. #847 #2655
  • Add :order to datetime_select, select_datetime and select_date. #1427
  • Make scaffolding work with database time values. #2489
  • Added time_select to work with time values in models. #2833 #2489
  • Added :include_seconds to select_datetime, datetime_select and time_select. #2998
  • All date/datetime selects can now accept an array of month names with :use_month_names. Allows for localization. #363
  • Adds :date_separator to select_datetime. Preserves BC.
  • Adds :time_separator to select_time. Preserves BC.

Rails acts_as_threaded Plugin 12

Posted by Bob Silva Mon, 06 Feb 2006 01:15:00 GMT

While tinkering around with the acts_as_nested_set act of ActiveRecord, I tried to build a threaded forum with it. I ran into some difficulties in that a nested_set is designed to only have one root. Obviously, in a threaded forum, each thread would be its own root. So after wasting a couple hours trying to make nested_set work, I decided to use what I consider to be the best part of Rails: plugins. "acts_as_threaded" was born 1 hour later.


While I used it to build a threaded forum in this demo, it's design was inspired by a different project at work. The acts_as_tree behavior could have worked, but I needed a better way of containing my children (much like in real life).


I've included an 11 minute screencast of building an ugly looking Threaded Forum, but the concepts are there. I can still remember spending many man hours with my programming team at Bravenet to design and build the forum product we offered there. If Rails existed back then, we could have built it in a day. (Leave me a comment on how the screencast turned out.)


Click the image to view the screencast of acts_as_threaded in use: (8 MB)




As usual, you can download the plugin here. I will submit it as a patch when I solidify it and create the tests.

If you are following the tutorial, here is the display code in full.

def display_threads(threads)
    content = ''
    for thread in threads
      content << content_tag('div',
                  content_tag('div',"* " + link_to("#{h thread.title}", {:action => 'show', :id => thread.id}, {'style' => 'color: #00f'}) + " " +
                  content_tag('span', " by #{h thread.name} · #{thread.created_at.strftime('%b, %d %Y - %I:%M %p')}", 'style' => 'font:10px tahoma;color:#666;')),
                  'style' => "margin:5px 0px;padding-left:#{thread.depth*20}")
    end
    content
  end


UPDATE: See how to do this without a plugin using native Rails functionality.

The Big Tease

Posted by Bob Silva Thu, 02 Feb 2006 03:02:00 GMT

So my 30" Apple Cinema Display arrived today and it is pretty awesome to look at. The problem is, thats all I can do with it. I have a Dell Inspiron 9200 laptop that has an ATI Modility Radeon 9700 in it, but it isn't able to power it at full resolution. It can only light it up at 1280x800. How disappointing. Even my laptops screen is 1440. So I guess I will have to wait until beginning of March for the MacBook Pro to arrive before I can really enjoy the display. What a bummer. On the flip side, I found out our FredEx guy has a pretty good sense of humor. When he delivered it to my door he says: "You have a towel? Might need to wipe it off a little. I've been drooling over it all day."

Ruby on Rails Increases Apples Revenue Stream

Posted by Bob Silva Tue, 31 Jan 2006 01:29:00 GMT

One of the problems with todays society is the close-mindedness of its occupants. It's especially prevalent in the programming world. I typically refer to them as zealots. I'll list the 3 groups that I see as the most damaging to my profession below. (I'll get back to the title of the article momentarily, keep an open-mind as you read this, it has a happy ending.)

Programming Language Evangelists

This group comes in a variety of breeds. C, C++, C# Java, PHP, Ruby, Perl, Python, Delphi and Visual Basic. Take your pick and you will find programmers who utterly refuse to believe there could be any other option. I have yet to talk with another programmer that changed languages based on the 'but look at all the cool things my language can do that yours can't' argument. If anything, it just reaffirms that their language choice is that much better.


The Open Source Cult

Giving back to the open-source community is a choice not a requirement. It's when you tell me I have to, that you cross the line. I had a heated debate with an open-source zealot at work a few months back when we were going to modify a GPL'd PHP application and use it to generate revenue from our customers. We were not planning on redistributing the application, just hosting it on our site. He was adamant that we were legally and morally bound to submit our changes back to the author. Well, you don't have to go to Harvard Business School to understand the concept of competitive advantage. We were doing this to generate revenue not to feel good about ourselves. Before you thrash me, let me say that ALL of us use open source products to generate revenue.

Here's his email that started it all (emphasis added):

Hi Bob,

GPL (General Public License):

excerpted:

"If you modify your copy or copies of the program or any portion of it, 
or develop a program based upon it, you may distribute the resulting 
work provided you do so under the GNU General Public License. Any 
translation of the GNU General Public License must be accompanied by the 
GNU General Public License."


**PHPSurveyor is, in fact, GPLed.

You would probably be better served by looking for code with a less 
restrictive license, if in fact you desire to incorporate it in a 
non-GPLed derivative. I would recommend looking for something licensed 
under the BSD scheme:

http://www.opensource.org/*license*s/*bsd*-*license*.php

Apple successfully used BSD licensed code (Darwin) to create their 
(very) proprietary OS X.

Hope this helps!

IANAL, but  I have a pretty extensive background  in licensing issues.
Now, I am not a lawyer either, but I'm pretty sure even a lawyer would understand the difference between the words *may* and *must*. And just to be sure, I emailed the FSF for verification and got a reply from Zak Greant (a volunteer):
Dear Bob,

The case you cite is not distribution under the terms and 
conditions of the GPL.

Thanks for writing,
Zak Greant

Vindicated! It's this kind of attitude and mis-understanding of the open-source movement that causes people to either love it or hate it. Life is full of choices, take away my choice and you take away my life.

MAC Lovers

These are the worst offenders of all. It's okay to believe in a product and have your own opinion of its merits over another platform, but to flat out refuse to accept that someone may have a different opinion or experience is a bit immature and ignorant. It's the my daddy can beat your daddy up argument from your childhood. For this specific reason, I have rejected even considering Mac for fear others might see me as a close-minded, immature idiot who's daddy got his butt whooped by his friend's Dad when he was 5 years old.

And now, the rest of the story...

I've used Windows exclusively since 1995. Literally, the only time I have touched a Mac was when I tried to turn one on about 7 years ago. After 5 minutes of looking for the freakin power button, I finally swallowed my pride and asked for help, after all, I'm a computer professional, I know how to turn on a computer! I wonder how many drinks an engineer at Apple had before he thought of putting the power button on the damn keyboard.

With that said, I'm at a point in my life where I'm trying things I never would have tried a few years ago. For instance, now I actually use the hot mustard they bring you with a plate of pork fried rice. On occasion, I even stick a few pieces of brocolli in my bowl at our local Mongolian restaurant.

Two weeks ago, I decided to put aside my trusty companion for so many years, PHP, and give Ruby a try; sorry PHP, you've been good to me, but its time to move on. Once again, I've reaped the rewards of having an open-mind and trying new things. Well, today, I made the biggest step of all. I'm not sure if someone spiked my Coke at lunch, but afterwards, I went back to my office and ordered a brand spanking new MacBook Pro with a 30" cinema display. While I'm a little anxious and a little excited, I do have one major concern about my decision.

Does it come with instructions for how to turn it on?



PS: I haven't told my wife yet, so keep it on the low down.

Older posts: 1 2 3 4 5