Create FFMPEG processor for Carrierwave in Rails 3

I have had the pleasure of working with the carrierwave gem recently (as opposed to paperclip), and I must say, I am quite the fan. Once major thing I missed however, was the available list of custom user plugins for it, unlike paperclip. I believe this is mostly due to how new and recent carrierwave is. That being said, I put together a simple example of a FFMPEG process that will allow me to resample the bitrate of a file. This should lay the ground work for other features as well. This example is using Rails 3, but should be easily adaptable for 2. Also, make sure you already have FFMPEG installed and running properly. So lets get started: First things first…we need to add the appropriate gems to our Gemfile:

# Gemfile
gem "carrierwave"
gem "streamio-ffmpeg"

Next is the meat and potatoes of this..the actual FFMPEG process for carrierwave. I choose to keep my plugin files in the directory lib/carrierwave. Make sure you have this path included in your application.rb file if you are using rails 3. Here is the code:

# lib/carrierwave/ffmpeg.rb
require "streamio-ffmpeg"
module CarrierWave
  module FFMPEG
    module ClassMethods
      def resample( bitrate )
        process :resample => bitrate
      end
    end
 
    def resample( bitrate )
      directory = File.dirname( current_path )
      tmpfile = File.join( directory, "tmpfile" )
      File.move( current_path, tmpfile )
      file = ::FFMPEG::Movie.new(tmpfile)
      file.transcode( current_path, :audio_bitrate => bitrate)
      File.delete( tmpfile )
    end
  end
end

Good. Now that we have the plugin coded up, we need to include it into our uploader. I already have one mounted to my Asset model. Here is what my AssetUploader now looks like:

# app/uploaders/asset_uploader.rb
require File.join(Rails.root, "lib", "carrier_wave", "ffmpeg")

class AssetUploader < CarrierWave::Uploader::Base
  include CarrierWave::FFMPEG # <= include the plugin
  
  # Choose what kind of storage to use for this uploader:
  storage :file

  # Override the directory where uploaded files will be stored.
  # This is a sensible default for uploaders that are meant to be mounted:
  def store_dir
    "#{Rails.root}/uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}"
  end
 
  # Add a version, utilizing our processor
  version :bitrate_128k do
    process :resample => "128k"
  end
end

There! Now whenever you add a new file, it should fire off the processor and create a new version. I hope this help anyone still up in the air about how to put together their own plugin/process for carrierwave. Next I will demonstrate how to incorporate Delayed::Job to move these intensive tasks to the background!

Moved to a new host!

I have spend the last week setting up a new VPS host at Linode. This site and my youtube video playlist site, http://www.jamzee.com, are both currently running on the new host. Everything has been great so far and this will give me more control in the future!

If you are interested in joining Linode, check out my referral link.

How to create PDF’s and Images from your website in Rails

I am going to show you how to generate both a pdf and image from a single action in a controller using the awesome, wkhtmltopdf library. This also uses PDFKit and WebSnap gems available on GitHub. This example assumes the following:

  • wkhtmltopdf and wkhtmltoimage are already installed and accessible on in the PATH.
  • You have an html page setup to display the record.
  • You have created a pdf CSS file to help display the pdf, if you so choose.
# config/initializers/mime_types.rb
Mime::Type.register "application/pdf", :pdf
Mime::Type.register "image/png", :png

# app/controllers/items_controller.rb
def show
  @item = Item.find(params[:id])
  
  respond_to do |format|
    format.html { }
    format.pdf {
      html = render(:action => "show.html.erb")
      
      kit = PDFKit.new( html, :zoom => 0.75 )
      kit.stylesheets << File.join(RAILS_ROOT,
                                   "public",
                                   "stylesheets",
                                   "pdf.css")
      
      send_data kit.to_pdf, :filename => "item.pdf",
                            :type => "application/pdf",
                            :disposition => "inline"
    }
    format.png {
      html = render :action => "show.html.erb",
                    :layout => "application.html.erb"

      # I am nil’ing these options out because my version of wkhtmltoimage does
      # not support the scale options and I do not want to crop the image.
      snap = WebSnap.new(html, :format => "png",
                               :"scale-h" => nil,
                               :"scale-w" => nil,
                               :"crop-h" => nil,
                               :"crop-w" => nil,
                               :quality => 100,
                               :"crop-x" => nil,
                               :"crop-y" => nil)

      send_data snap.to_bytes, :filename => "item.png",
                               :type => "image/png",
                               :disposition => "inline"
    }
  end
end

Now you should be able to access three distinct views, each producing a different result

http://example.com/items/1 # => Generates an html page.
http://example.com/items/1.pdf # => Generates a pdf of the html page.
http://example.com/items/1.png # => Generates a png of the html page.

You could easily also add more image types by just created another block for each format, and changing the :format to whatever one you would like.

Request formats, filters, and functional tests…

I recently had to write some tests against a controller that was filtering based on the requesting format. In this case, I wanted to allow xml requests only, and redirect to login on everything else. This was fine when browsing or using curl by doing a simple:

skip_before_filter :login_required,
  :only => [:create],
  :if => Proc.new {|c| c.request.format.xml?}

My problem came when I was trying to create tests to verify that both html and xml requests did in fact produce the correct response. After many hours of messing around, I came up with a simple solution. First I skip the filters for every request, then I have another filter to re-enable them on anything but the xml request:

skip_before_filter :login_required, :only => [:create]
before_filter :only => [:create] do |c|
  c.send(:login_required) unless c.request.format.xml?
end

Voila!!! This allowed me to continue with my rails testing and browser and curl work appropriately. (I use curl to test the xml request).

def test_not_logged_in_normal_post
  post :create, :login => "test@test.com", :password => "test"
  assert_response :redirect
end

def test_not_logged_in_xml_post
  post :create, :format => "xml", :login => "test@test.com", :password => "test"
  assert_response :success
end