Question

I am using carrierwave (0.9.0) with fog (1.18.0), mini_magick (3.6.0), and Rackspace CloudFiles. I want to composite images.

I have two models: Products and Emails. In the products uploader I am trying to create a process that loops through all the emails and composites the product image on top of each email.background_image. I also want to control the name of this image so that is:

"#{email.name}_#{product.product_number}.jpg"

I have included my process and method below. Test result in a file being uploaded to cloudfiles named "email_123.png" and the original image. The name doesn't currently include the email's name and images are not compositing, I just receive the original back. Anything obvious I am missing here? Thanks!

version :email do
  process :email_composite

  def email_composite
    Email.all.each do |email|
      email_image = email.secondary.url
      email_name = email.name
      merge(email_name, email_image)
    end
  end

  def merge(email_name, email_image)
    manipulate! do |img|
      @product = Product.find(model.id)
      email_image = ::MiniMagick::Image.open(email_image)

      img = email_image.composite(img, "jpg") do |c|
        c.gravity "Center"
      end
      # img = yield(img) if block_given?
      img = img.name "#{email_name}_#{@product.product_number}.png"

      img
    end
  end
end

UPDATED

First I wanted to thank you for your thorough answer, the amount of effort involved is obvious and appreciated.

I had a few more questions when trying to implement this solution. Inside the #compose method you have:

email_image = ::MiniMagick::Image.open(email_image)

Should this be:

email_image = ::MiniMagick::Image.open(email_image_url)

I assume this from seeing the define_method call included below ( which was awesome by the way - I didn't know you could do that):

define_method(:email_image_url) { email.secondary.url }

After getting Undefined method "versions_for" I switched the #versions_for method as I will always want all email to have a secondary version.

versions_for before

def self.versions_for emails
  reset_versions!
  email.find_each { |e| version_for_email(e) }
end

versions_for after changes

def self.versions_for
  reset_versions!
  Email.all.find_each { |e| version_for_email(e) }
end

This change allowed me get rid of the error.

The end result of all of this is that everything appears as if it works, I can upload imagery without error, but now I have no resulting image for the emails versions. I still get my other versions like thumb etc. Any idea what could cause this? Again thanks for all the help you have provided thus far.

Was it helpful?

Solution

Calling manipulate! multiple times within the same version applies the changes in its block to the same image; what you want is to create many versions, one corresponding to each Email. This is tricky because CarrierWave really wants you to have a small set of statically defined versions in your code; it actually builds a new, anonymous Uploader class to handle each version.

We can trick it to build versions dynamically, but it's pretty ugly. Also, we have to be careful to not keep references to stale uploader classes around, or we'll accumulate classes endlessly and eventually run out of memory!

# in ProductUploader:

# CarrierWave stores its version classes in two places:
#
# 1. In a @versions hash, stored within the class; and
# 2. As a constant in your uploader, with a name based on its #object_id.
#
# We have to clean out both of them to be sure old versions get garbage
# collected!
def self.reset_versions!
  versions.keys.select { |k| k =~ /^email_/ }.each do |k|
    u = versions.delete(k)[:uploader]
    remove_const("Uploader#{u.object_id}".gsub('-', '_'))
  end
end

def self.version_name_for email
  "email_#{email.name}".to_sym
end

# Dynamically generate the +version+ that corresponds to the image composed
# with the image from +email+.
def self.version_for_email email
  version(version_name_for(email)) do
    process :compose

    # Use +define_method+ here so that +email+ in the block refers to the
    # argument "email" from the outer scope.
    define_method(:email_image_url) { email.secondary.url }

    # Use the same trick to override the filename for this version.
    define_method(:filename) { "#{email_name}_#{model.product_number}.png" }

    # Compose +email_image+ on top of the product image.
    def compose
      manipulate! do |img|
        email_image = ::MiniMagick::Image.open(email_image_url)

        # Do the actual composition.
        img = email_image.composite(img, "jpg") do |c|
          c.gravity "Center"
        end

        img
      end
    end
  end
end

# Clear out any preexisting versions and generate new ones based on the
# contents of +emails+.
def self.versions_for emails
  reset_versions!
  email.find_each { |e| version_for_email(e) }
end

# After you call Product.versions_for(emails), you can use this to fetch
# the version for a specific email:
def version_for_email email
  versions[self.class.version_name_for(email)]
end

To use this, be sure to call Product.versions_for(Email.all) (possibly in a before_filter), Then, you can access the version for a specific email with @p.product.version_for(email):

# in your controller:

def action
  # Recreate a +version+ for each email.
  # If you don't want to overlay *every* email's image, you can also pass a
  # subset here.
  ProductUploader.versions_for(Email.all)

  # Upload a file and its versions to Cloud Files...
  @p = Product.find(params[:id])
  @p.product = File.open(Rails.root.join('clouds.jpg'), 'r')
  @p.save!
end

In your views, you can use url or any other helpers as usual:

# in a view:

<% Email.all.find_each do |email| %>
  <%= image_tag @p.product.version_for_email(email).url %>
<% end %>
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top