Home

String#to_file: the easiest way to write a string to a file in Ruby

The Ruby file API has always struck me as inelegant (i.e., I’m constantly looking up its syntax). So I wrote String#to_file to make the common operation of writing a string to a file easy:

class String
  def to_file(filename)
    File.open(filename, 'w') {|f| f.write self }
  end
end

Now when you do this:

"some string".to_file("testing.txt")

You’ll get a file called “testing.txt” that contains “some string”. Easy, no?

You can use the same method to download files:

require 'open-uri'
url = 'http://blog.stackoverflow.com/audio/stackoverflow-podcast-001.mp3'
open(url).read.to_file(url.split('/').last)

And boom, you’ve downloaded the file to “stackoverflow-podcast-001.mp3”

Posted November 18th, 2010

Rails Bug: Changes to has_many associations are saved immediately (irrespective of whether you save the parent object)

Here’s a simple Post model:

class Post < ActiveRecord::Base
  has_many :comments, :dependent => :destroy
  validates_presence_of :title
end

Now check out what happens when I try to save an invalid post (I’m logging to stdout so you can see the SQL):

[Dev]> post.comments.count
=> 1
[Dev]> post.update_attributes(:title => nil, :comments => [])
  Comment Load (0.2ms)  SELECT "comments".* FROM "comments" WHERE ("comments".post_id = 2)
  Post Load (0.1ms)  SELECT "posts".* FROM "posts" WHERE ("posts"."id" = 2) LIMIT 1
  SQL (0.1ms)  DELETE FROM "comments" WHERE ("comments"."id" = 1)
=> false
[Dev]> post.errors.full_messages
=> ["Title can't be blank"]
[Dev]> post.comments.count
=> 0

The update fails, of course, because the post’s title cannot be blank. However, note this line of SQL:

SQL (0.1ms)  DELETE FROM "comments" WHERE ("comments"."id" = 1)

Even though this save failed, Rails still deleted all the post’s comments! And it had nothing to do with the update_attributes method; merely setting the comments attribute kills all existing comments:

[Dev]> post.comments.count
=> 1
[Dev]> post.comments = []
  Comment Load (0.2ms)  SELECT "comments".* FROM "comments" WHERE ("comments".post_id = 2)
  Post Load (0.1ms)  SELECT "posts".* FROM "posts" WHERE ("posts"."id" = 2) LIMIT 1
  SQL (0.1ms)  DELETE FROM "comments" WHERE ("comments"."id" = 1)
=> []
[Dev]> post.comments.count
=> 0

This is extremely counter-intuitive: post.title = "whatever" doesn’t affect the post’s title until I save, and neither should post.comments = some_comments.

Is there a workaround?!

Yes: enclose all statements that set an attribute corresponding to a has_x association in a transaction:

Post.transaction do
  post.comments = some_comments
  post.save!
end

This way, if save! throws an exception, the transaction will roll back the changes you made to the post’s comments

It’s annoying and error-prone to have to remember to use transactions; hopefully Rails' default behavior changes someday.

Posted November 17th, 2010

Why photo rotation is broken on the iPhone 4 and how to fix it (using Paperclip on Heroku)

Apple made an annoying change to the way the iPhone camera works in iOS4. If you’ve ever been emailed a photo from an iPhone 4, you’ve experienced this rage guy situation:

Photo looks fine in Gmail's preview..

But when you click 'View' it's rotated incorrectly

What’s going on?

Exchangeable image file format (EXIF)

EXIF is a standard method for storing metadata in image files. What’s metadata? Data about the photo (as opposed to the data that makes up the photo itself) — example pieces of metadata stored in EXIF are:

  • When the photo was taken
  • Where the photo was taken (if your camera has GPS)
  • The camera model
  • (Relevant to this iPhone issue) how programs viewing the photo should rotate it for display

Before EXIF, if you wanted to store the date a photo was taken with the photo, you’d have to print it on the photo itself (ugly!):

EXIF allows you to store this kind of data without affecting the way a photo looks (kind of like writing a date on the back of a print)

What’s causing the problem?

Here’s what happens:

  1. The iPhone camera sensor's "up" is the right edge of the phone (when held with the home button at the bottom).

    When the iPhone takes a photo, it writes the photo data with the default sensor orientation (i.e., as if the photo had been taken with the phone rotated such that the home button is on the right.)

  2. The iPhone saves the real orientation as EXIF data with the photo
  3. Software that's EXIF-aware (the iPhone's "Photos" app, Gmail's photo thumbnailing logic) rotates the photo correctly for display, whereas software that's not EXIF-aware (Firefox) doesn't

Why doesn’t the iPhone write the photo data with the correct orientation? In fact, it seems like there shouldn’t even be the option to specify rotation in EXIF! I.e., if there were only one way of specifying a photo’s orientation (the orientation of the photo data itself), consumers wouldn’t have to worry about whether their photo viewing software knew about EXIF.

The way to fix this (and this is what the previous iPhone OS did) is to rotate the actual photo data when it comes off the sensor; unfortunately this operation is computationally expensive, and so I assume Apple stopped doing it so the iPhone would save photos faster (perhaps this is one reason the iOS4 camera app is so much more responsive)

How do I fix this in my application?

Here’s how I fixed this problem for InstaGal, which uses Paperclip for attachments and is hosted on Heroku.

The basic idea is to use the EXIF orientation information to rotate the actual photo before saving. Fortunately, the ImageMagick library (which Paperclip uses) has a convenience “auto-orient” method that saves you the trouble of manually reading the EXIF data and making the appropriate rotation.

How do we get Paperclip to use ImageMagick’s auto-orientation? Google around and you’ll see recommendations to add :convert_options => { :all => '-auto-orient' } to your Paperclip configuration like so:

has_attached_file :image, :storage => :s3,
                  :styles => { :medium => "600x600>", :small => "320x320>", :thumb => "100x100#" },
                  :convert_options => { :all => '-auto-orient' },
                  :s3_credentials => "#{RAILS_ROOT}/config/s3.yml",
                  :path => "/:style/:id_:filename"

This supposedly passes the “auto-orient” option to ImageMagick before it creates your thumbnails, but it didn’t work for me on Heroku, and messing with ImageMagick command line options is a bit hacky.

Instead, we’ll use RMagick (the Ruby version of ImageMagick) in a separate image processor.

First, create the file config/initializers/auto_orient.rb:

module Paperclip
  class AutoOrient < Paperclip::Processor 
    require 'RMagick' # Make sure to update your gem file

    def initialize(file, options = {}, *args)
      @file = file
    end

    def make( *args )
      img = Magick::Image.read("#{File.expand_path(@file.path)}")[0]
      img.auto_orient!

      temp = Tempfile.new(@file.path.split('/').last)
      img.write(temp.path)
      return temp
    end
  end
end

Now, update your Paperclip settings:

has_attached_file :image, :storage => :s3,
                  :styles => { :original => '5000x5000>', :medium => "600x600>", :small => "320x320>", :thumb => "100x100#" },
                  :processors => [:auto_orient, :thumbnail],
                  :s3_credentials => "#{RAILS_ROOT}/config/s3.yml",
                  :path => "/:style/:id_filename"

Note:

  1. The auto_orient processor runs before the thumbnail processor
  2. I’ve created a style called “original” so that my originals get auto-oriented as well (this is good practice anyway to prevent people from attaching arbitrarily large images)

And that’s it! Paperclip will automatically correct the orientation of your attached photos before resizing and saving them.

Posted November 16th, 2010