DZone Snippets is a public source code repository. Easily build up your personal collection of code snippets, categorize them with tags / keywords, and share them with the world

Snippets has posted 5883 posts at DZone. View Full User Profile

Export Adobe FDF And XFDF From ActiveRecord

02.09.2007
| 8652 views |
  • submit to reddit
        I needed to export XFDF from an application.  This code is kind of untested, but is based on the solution I came up with.  I would appreciate responses/modifications.  It's very straightforward mixin stuff, for the most part.
# (X)FDF Export for ActiveRecord
# Based on Justin Koivisto's FDF library for PHP
# Author: Sean Cribbs, seancribbs_AT_gmail_DOT_com, http://seancribbs.com
module FDF
  def self.included(base)
    base.extend ClassMethods
  end
  
  module ClassMethods
    # Options:
    #  <tt>:filename</tt> - The filename of the associated PDF document.  REQUIRED!
    #  <tt>:indentation</tt> - How much to indent the resulting XFDF (XML)
    #  <tt>:include</tt> - Which associated models to include in the generated XFDF.
    #  <tt>:exclude_attributes</tt> - Which attributes of the current model should not be exported.  By default all non-internal attributes are exported (i.e. everything but _id fields).
    #  <tt>:include_attributes</tt> - Which attributes of the current model should be exported in addition to the default. By default all non-internal attributes are exported (i.e. everything but _id fields).
    #  <tt>:attributes</tt> - Override which attributes to export.
    def exports_xfdf(options = {})
      raise ArgumentError, "A :filename option must be specified." unless options[:filename]
      options[:indentation] ||= 2
      options[:include] = options[:include].is_a?(Array) ? options[:include] : [options[:include]].compact
      unless included_modules.include? XFDFMethods
        class_inheritable_accessor :xfdf_options
        extend ClassMethods
        include XFDFMethods
      end
      self.xfdf_options = options
    end
    
    # Options:
    #  <tt>:filename</tt> - The filename of the associated PDF document.  REQUIRED!
    #  <tt>:include</tt> - Which associated models to include in the generated FDF.
    #  <tt>:exclude_attributes</tt> - Which attributes of the current model should not be exported.  By default all non-internal attributes are exported (i.e. everything but _id fields).
    #  <tt>:include_attributes</tt> - Which attributes of the current model should be exported in addition to the default. By default all non-internal attributes are exported (i.e. everything but _id fields).
    #  <tt>:attributes</tt> - Override which attributes to export.
    def exports_fdf(options = {})
      raise ArgumentError, "A :filename option must be specified." unless options[:filename]
      options[:include] = options[:include].is_a?(Array) ? options[:include] : [options[:include]].compact
      unless included_modules.include? FDFMethods
        class_inheritable_accessor :fdf_options
        extend ClassMethods
        include FDFMethods
      end
      self.fdf_options = options
    end
  end

  module XFDFMethods
    def to_xfdf(options = {})
      options.reverse_merge! self.class.xfdf_options
      fields = Util.collect_values(self, self.class.content_columns.map(&:name), options)
      filename = options[:filename]
      xml = Builder::XmlMarkup.new :indentation => options[:indentation]
      xml.instruct!
      xml.xfdf("xmlns" => "http://ns.adobe.com/xfdf/", "xml:space" => "preserve") {
        xml.f :href => filename
        xml.fields {
          fields.each do |field, value|
            xml.field(:name => field) {
              if value.is_a? Array
                  value.each {|item| xml.value(item.to_s) }
              else
                xml.value(value.to_s)
              end
            }
          end
        }
      }
      xml.target!
    end    
  end
  
  module FDFMethods
    def to_fdf(options={})
      options.reverse_merge! self.class.fdf_options
      fields = Util.collect_values(self, self.class.content_columns.map(&:name), options)
      filename = options[:filename]
      data = "%FDF-1.2\n%âã�?Ó\n1 0 obj\n<< \n/FDF << /Fields [ "
      fields.each do |field, value|
        if value.is_a? Array
          data << "<</T(#{field})/V["
          value.each {|v| data << "(#{v.strip})"}
          data << "]>>"
        else
          data << "<</T(#{field})/V(#{value.strip})>>"
        end
      end
    end
    data << "] \n/F (#{filename}) /ID [ <#{MD5.md5(Time.now).to_s}>\n ] >>" <<
            " \n>> \nendobj\ntrailer\n" << "<<\n/Root 1 0 R \n\n>>\n%%EOF\n"
  end
  
  module Util
    def self.collect_values(object, defaults, options = {})
      attrs = []
      if options[:attributes]
        attrs = stringify_all(options[:attributes]) rescue []
      else        
        [:include_attributes, :exclude_attributes].each do |opt|
          options[opt] = stringify_all(options[opt]) rescue []
        end
        attrs = stringify_all(defaults) + options[:include_attributes] - options[:exclude_attributes]
      end
      fields = attrs.inject({}) do |hash, key|
        value = object.send(key) rescue nil
        hash.merge key => value
      end
      fields.merge collect_association_values(object, options)
    end
    
    def self.collect_association_values(object, options = {})
      return {} if options[:include].blank?
      values = {}
      options[:include].each do |association|
        unless object.send(association).blank?
          models = object.send(association)
          unless models.is_a? Array
            columns = models.class.content_columns.map(&:name)
            values.merge! association_dump(association, models, columns)
          else
            models.each_with_index do |model, index|
              columns = model.class.content_columns.map(&:name)
              values.merge! association_dump("#{association.singularize}_#{index+1}", model, columns)
            end
          end
        end
      end
      values
    end
    
    def self.association_dump(prefix, object, attributes)
      attributes.inject({}) do |hash, attr|
        value = object.send(attr) rescue nil
        hash.merge "#{prefix}_#{attr}" => value
      end
    end
    
    def self.stringify_all(ary)
      ary.compact.map(&:to_s).uniq
    end
  end
end
ActiveRecord::Base.send :include, FDF

Also available from: http://pastie.caboo.se/38835