Injecting image= method into model classes


#1

hi All,

I’ve been trying to wrestle dynamic code generation into place for a
week now with no success. What I want is a module or class (started
trying to use module but finished up trying to use a class) which can
inject a method into its consumer which will allow:

user.image = #Ruby:File

as you would get from a file upload. (This eliminates any need for
special image handling code in the view or controller and lets the image
field act like any other). It uses the RMagick library to convert all
images to jpg’s and resize them to a consumer-defined size. What I was
hoping the group could help with was moving all the code out of the
model and into my ImageBlob class (which will inject necessary code into
the model (i.e., consumer)). Here’s what I currently have (which
works):

model.rb

def image=( file )
write_attribute( ‘image’, ImageBlob.getBlob( file, 225 ))
end
def thumbnailImage=( file )
write_attribute( ‘thumbnailImage’, ImageBlob.assignAttrib( file, 125
))
end

ImageBlob.rb

require ‘RMagick’
include Magick

class ImageBlob
def ImageBlob.getBlob( file, maxImageSize )
#omitted code which resizes and reformats image and returns blob
end
end

I was hoping to have an initialize function in ImageBlob which would
take the consumer class and inject the necessary methods into it (image=
and thumbnailImage=). Here’s my best attempt at that (which fails for
an unknown reason):

def initialize( attributesAndSizesHash, consumerClass )
attributesAndSizesHash.each_pair {|attributeName, maxImageSize|
code = “class #{consumerClass}
def #{attributeName}=( file )
write_attribute( ‘#{attributeName}’, ImageBlob.getBlob(
file, #{maxImageSize} ))
end
end”
eval( code ) }
end

I also tried making code define a module and calling
consumerClass.extend( moduleName ). Neither of these approaches are
working.

Of course, if this implemention could work, then consumers of the
ImageBlob class could simply call:

ImageBlob.new( { “image” => 225, “thumbnailImage” => 125 }, self )

anywhere in their class definition to have the functionality added (with
any number of image fields each with their own size).

Can anyone out there provide the last piece of this puzzle? If it is
provided, I will post the entire source for this module and it should
prove to be useful.

Regards,
Jonathan


#2

Anyone have an idea? A simple example of how to inject code into a
generic class would probably suffice. Can you possibly do a mixin
dynamically and, if so, how?

Much thanks.


#3

Hi !

2005/12/3, Jonathan removed_email_address@domain.invalid <removed_email_address@domain.invalid.
removed_email_address@domain.invalid:

Anyone have an idea? A simple example of how to inject code into a
generic class would probably suffice. Can you possibly do a mixin
dynamically and, if so, how?

Have you looked at FileColumn ? It does what you want, except it uses
the file system. If you were to create a way to store in the DB
instead, I’m sure Sebastian would include that in a future release.

See http://www.kanthak.net/opensource/file_column/

Bye !


#4

On 12/4/05, Jonathan removed_email_address@domain.invalid <removed_email_address@domain.invalid.
removed_email_address@domain.invalid wrote:

Anyone have an idea? A simple example of how to inject code into a
generic class would probably suffice. Can you possibly do a mixin
dynamically and, if so, how?

well, I don’t have to plug my file_column plugin anymore since
Francois was already so nice to do so :slight_smile: However, you might want to
take a look at the plugin’s source code as it does exactly this:
adding methods to a model dynamically.

Instead of creating strings and evaluating them, I would suggest you
take a look at Module#define_method that let’s you dynamically define
methods using ruby’s nice block syntax. A small example from
file_column, that defines the “image=” method. (where the name of the
attribute is stored in the local variable attr).

 define_method "#{attr}=" do |file|
    state = send(state_method).assign(file)
    instance_variable_set state_attr, state
    if state.options[:after_upload] and state.just_uploaded?
      state.options[:after_upload].each do |sym|
        self.send sym
      end
    end
  end

“state_method” is a Symbol that contains the name of another generated
method that will return an object that does all the heavy lifting for
file uploads/image manipulation.

As a last note I wanted to give the reason why I decided to store
files in the filesystem and not in the database: Simply put, rails’
support for large binary columns is very poor and inefficient
(somebody correct me please if that has changed since I last checked)
and since the filesystem is very good at storing files I decided to
leverage this.

Sebastian


#5

Oops. The thumbnailImage= method should have been defined as this:

def thumbnailImage=( file )
write_attribute( ‘thumbnailImage’, ImageBlob.getBlob( file, 125 ))
end


#6

skanthak wrote:

On 12/4/05, Jonathan removed_email_address@domain.invalid <removed_email_address@domain.invalid.
removed_email_address@domain.invalid wrote:

Anyone have an idea? A simple example of how to inject code into a
generic class would probably suffice. Can you possibly do a mixin
dynamically and, if so, how?

well, I don’t have to plug my file_column plugin anymore since
Francois was already so nice to do so :slight_smile: However, you might want to
take a look at the plugin’s source code as it does exactly this:
adding methods to a model dynamically.

Instead of creating strings and evaluating them, I would suggest you
take a look at Module#define_method that let’s you dynamically define
methods using ruby’s nice block syntax. A small example from
file_column, that defines the “image=” method. (where the name of the
attribute is stored in the local variable attr).

 define_method "#{attr}=" do |file|
    state = send(state_method).assign(file)
    instance_variable_set state_attr, state
    if state.options[:after_upload] and state.just_uploaded?
      state.options[:after_upload].each do |sym|
        self.send sym
      end
    end
  end

“state_method” is a Symbol that contains the name of another generated
method that will return an object that does all the heavy lifting for
file uploads/image manipulation.

As a last note I wanted to give the reason why I decided to store
files in the filesystem and not in the database: Simply put, rails’
support for large binary columns is very poor and inefficient
(somebody correct me please if that has changed since I last checked)
and since the filesystem is very good at storing files I decided to
leverage this.

Sebastian

Thank you very much. :slight_smile: define_method should do the trick. In my case,
the client has requested that files be stored in the database to
eliminate file permission complexities. How does your plugin work with
file permissions (i.e., which user owns the files and where are they
stored)?


#7

I actually decided try class_eval first (which David Black suggested on
the Ruby list). It worked, so I had no need to investigate further. I
figured I’d share the results with everyone in case someone else can
find a use for this (it actually should be a fairly common task).

Here it is:

ImageBlob.rb

require ‘RMagick’
include Magick

class ImageBlob

def ImageBlob.inject_attrib_assign_methods( attr_and_sizes_hash,
consumer_class )
attr_and_sizes_hash.each {|attr_name, max_size|
code = %{
def #{attr_name}=( file )
write_attribute( ‘#{attr_name}’, ImageBlob.getBlob( file,
#{max_size} ))
end
}
consumer_class.class_eval code }
end

def ImageBlob.getBlob( file, max_image_size )
begin
img = Image.from_blob( file.read ).first
if not img
raise
end

  img.format = "JPG"

  if img.rows > max_image_size or img.columns > max_image_size
    if img.rows > img.columns
      factor = max_image_size / img.rows.to_f
    else
      factor = max_image_size / img.columns.to_f
    end

    img = img.scale( factor )
  end

  retVal = img.to_blob
  GC.start
  return retVal
rescue
  return nil
end

end
end

To use the class, from your model do:

ImageBlob.inject_attrib_assign_methods( { “image” => 225,
“thumbnailImage” => 125 }, self )

–Jonathan