diff --git a/app/models/outfit.rb b/app/models/outfit.rb index a8fe63bb..ef80f1ac 100644 --- a/app/models/outfit.rb +++ b/app/models/outfit.rb @@ -14,6 +14,8 @@ class Outfit < ActiveRecord::Base scope :wardrobe_order, order('starred DESC', :name) mount_uploader :image, OutfitImageUploader + + after_commit :enqueue_image! def as_json(more_options={}) serializable_hash :only => [:id, :name, :pet_state_id, :starred], @@ -68,23 +70,35 @@ class Outfit < ActiveRecord::Base end # Returns the array of SwfAssets representing each layer of the output image, - # ordered from bottom to top. - def layered_assets - visible_assets.sort { |a, b| a.zone.depth <=> b.zone.depth } + # ordered from bottom to top. Careful: this method is memoized, so if the + # image layers change after its first call we'll get bad results. + def image_layers + @image_layers ||= visible_assets_with_images.sort { |a, b| a.zone.depth <=> b.zone.depth } end - # Creates and writes the thumbnail images for this outfit. (Writes to file in - # development, S3 in production.) Runs #save! on the record, so any other - # changes will also be saved. + # Creates and writes the thumbnail images for this outfit iff the new image + # would be different than the current one. (Writes to file in development, + # S3 in production.) If the image is updated, updates the image layers hash + # and runs #save! on the record, so any other changes will also be saved. def write_image! - Tempfile.open(['outfit_image', '.png']) do |image| - create_image! image - self.image = image - save! + if image_layers_dirty? + Tempfile.open(['outfit_image', '.png']) do |image| + create_image! image + self.image_layers_hash = generate_image_layers_hash + self.image = image + save! + end end + self.image end + # Enqueue an image write iff the new image would be different than the + # current one. + def enqueue_image! + Resque.enqueue(OutfitImageUpdate, id) if image_layers_dirty? + end + def s3_key(size) URI.encode("#{id}/#{size.join 'x'}.png") end @@ -108,13 +122,13 @@ class Outfit < ActiveRecord::Base # Creates a 600x600 PNG image of this outfit, writing to the given output # file. def create_image!(output) - layers = self.layered_assets - base_layer = layers.shift + base_layer = image_layers.first + above_layers = image_layers[1..-1] write_temp_swf_asset_image! base_layer, output output.close Tempfile.open(['outfit_overlay', '.png']) do |overlay| - layers.each do |layer| + above_layers.each do |layer| overlay.open write_temp_swf_asset_image! layer, overlay overlay.close @@ -151,6 +165,21 @@ class Outfit < ActiveRecord::Base all_assets.select { |a| (1 << (a.zone_id - 1)) & restricted_zones_mask == 0 } end + def visible_assets_with_images + visible_assets.select(&:has_image?) + end + + # Generate 8-char hex digest representing visible image layers for this outfit. + # Hash function should be decently collision-resistant. + def generate_image_layers_hash + @generated_image_layers_hash ||= + Digest::MD5.hexdigest(image_layers.map(&:id).join(',')).first(8) + end + + def image_layers_dirty? + generate_image_layers_hash != self.image_layers_hash + end + IMAGE_BASE_SIZE = [600, 600] def write_temp_swf_asset_image!(swf_asset, file) key = swf_asset.s3_key(IMAGE_BASE_SIZE) diff --git a/app/models/outfit_image_update.rb b/app/models/outfit_image_update.rb new file mode 100644 index 00000000..85ce8ac2 --- /dev/null +++ b/app/models/outfit_image_update.rb @@ -0,0 +1,8 @@ +class OutfitImageUpdate + @queue = :outfit_image_updates + + def self.perform(id) + Outfit.find(id).write_image! + end +end + diff --git a/config/initializers/carrierwave.rb b/config/initializers/carrierwave.rb index 12b067dc..264719dc 100644 --- a/config/initializers/carrierwave.rb +++ b/config/initializers/carrierwave.rb @@ -3,7 +3,7 @@ # this to use S3 on all environments for those images only.) CarrierWave.configure do |config| - if Rails.env.production? || true # REMOVE + if Rails.env.production? s3_config = YAML.load_file Rails.root.join('config', 'aws_s3.yml') access_key_id = s3_config['access_key_id'] secret_access_key = s3_config['secret_access_key'] diff --git a/db/migrate/20120716193946_add_image_layers_hash_to_outfit.rb b/db/migrate/20120716193946_add_image_layers_hash_to_outfit.rb new file mode 100644 index 00000000..d5e04cef --- /dev/null +++ b/db/migrate/20120716193946_add_image_layers_hash_to_outfit.rb @@ -0,0 +1,9 @@ +class AddImageLayersHashToOutfit < ActiveRecord::Migration + def self.up + add_column :outfits, :image_layers_hash, :string, :length => 8 + end + + def self.down + remove_column :outfits, :image_layers_hash + end +end diff --git a/db/schema.rb b/db/schema.rb index 5d866571..556128eb 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended to check this file into your version control system. -ActiveRecord::Schema.define(:version => 20120521164652) do +ActiveRecord::Schema.define(:version => 20120716193946) do create_table "auth_servers", :force => true do |t| t.string "short_name", :limit => 10, :null => false @@ -116,8 +116,9 @@ ActiveRecord::Schema.define(:version => 20120521164652) do t.datetime "created_at" t.datetime "updated_at" t.string "name" - t.boolean "starred", :default => false, :null => false + t.boolean "starred", :default => false, :null => false t.string "image" + t.string "image_layers_hash" end add_index "outfits", ["pet_state_id"], :name => "index_outfits_on_pet_state_id" diff --git a/public/outfits/.gitignore b/public/outfits/.gitignore new file mode 100644 index 00000000..d6b7ef32 --- /dev/null +++ b/public/outfits/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore