forked from OpenNeo/impress
Matchu
42827362b6
Use the ImageMagick flatten command to generate the output all at once instead of compositing each layer individually, and download the layers in parallel. On my box, saving roopal27 five times took a total of 30 seconds before, whereas now it takes 7 seconds. I expect it to be even better on the production box, where latency is even lower.
217 lines
6.9 KiB
Ruby
217 lines
6.9 KiB
Ruby
class Outfit < ActiveRecord::Base
|
|
has_many :item_outfit_relationships, :dependent => :destroy
|
|
has_many :worn_item_outfit_relationships, :class_name => 'ItemOutfitRelationship',
|
|
:conditions => {:is_worn => true}
|
|
has_many :worn_items, :through => :worn_item_outfit_relationships, :source => :item
|
|
belongs_to :pet_state
|
|
belongs_to :user
|
|
|
|
validates :name, :presence => {:if => :user_id}, :uniqueness => {:scope => :user_id, :if => :user_id}
|
|
validates :pet_state, :presence => true
|
|
|
|
attr_accessible :name, :pet_state_id, :starred, :worn_and_unworn_item_ids
|
|
|
|
scope :wardrobe_order, order('starred DESC', :name)
|
|
|
|
mount_uploader :image, OutfitImageUploader
|
|
|
|
before_save :update_enqueued_image
|
|
after_commit :enqueue_image!
|
|
|
|
def as_json(more_options={})
|
|
serializable_hash :only => [:id, :name, :pet_state_id, :starred],
|
|
:methods => [:color_id, :species_id, :worn_and_unworn_item_ids,
|
|
:image_versions, :image_enqueued, :image_layers_hash]
|
|
end
|
|
|
|
def image_versions
|
|
{}.tap do |versions|
|
|
versions[:large] = image.url
|
|
image.versions.each { |name, version| versions[name] = version.url }
|
|
end
|
|
end
|
|
|
|
def closet_item_ids
|
|
item_outfit_relationships.map(&:item_id)
|
|
end
|
|
|
|
def color_id
|
|
pet_state.pet_type.color_id
|
|
end
|
|
|
|
def species_id
|
|
pet_state.pet_type.species_id
|
|
end
|
|
|
|
def to_query
|
|
{
|
|
:closet => closet_item_ids,
|
|
:color => color_id,
|
|
:objects => worn_item_ids,
|
|
:species => species_id,
|
|
:state => pet_state_id
|
|
}.to_query
|
|
end
|
|
|
|
def worn_and_unworn_item_ids
|
|
{:worn => [], :unworn => []}.tap do |output|
|
|
item_outfit_relationships.each do |rel|
|
|
key = rel.is_worn? ? :worn : :unworn
|
|
output[key] << rel.item_id
|
|
end
|
|
end
|
|
end
|
|
|
|
def worn_and_unworn_item_ids=(all_item_ids)
|
|
new_rels = []
|
|
all_item_ids.each do |key, item_ids|
|
|
worn = key == 'worn'
|
|
unless item_ids.blank?
|
|
item_ids.each do |item_id|
|
|
rel = ItemOutfitRelationship.new
|
|
rel.item_id = item_id
|
|
rel.is_worn = worn
|
|
new_rels << rel
|
|
end
|
|
end
|
|
end
|
|
self.item_outfit_relationships = new_rels
|
|
end
|
|
|
|
# Returns the array of SwfAssets representing each layer of the output image,
|
|
# 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 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!
|
|
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
|
|
self.image_enqueued = false
|
|
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)
|
|
end
|
|
|
|
def update_enqueued_image
|
|
self.image_enqueued = (image_layers_dirty?)
|
|
true
|
|
end
|
|
|
|
def s3_key(size)
|
|
URI.encode("#{id}/#{size.join 'x'}.png")
|
|
end
|
|
|
|
def self.build_for_user(user, params)
|
|
Outfit.new.tap do |outfit|
|
|
name = params.delete(:name)
|
|
starred = params.delete(:starred)
|
|
anonymous = params.delete(:anonymous) == "true"
|
|
if user && !anonymous
|
|
outfit.user = user
|
|
outfit.name = name
|
|
outfit.starred = starred
|
|
end
|
|
outfit.attributes = params
|
|
end
|
|
end
|
|
|
|
protected
|
|
|
|
# Creates a 600x600 PNG image of this outfit, writing to the given output
|
|
# file.
|
|
def create_image!(output)
|
|
unless image_layers.empty?
|
|
temp_image_files = Parallel.map(image_layers, :in_threads => 8) do |swf_asset|
|
|
image_file = Tempfile.open(['outfit_layer', '.png'])
|
|
write_temp_swf_asset_image!(swf_asset, image_file)
|
|
image_file.close
|
|
image_file
|
|
end
|
|
|
|
# Here we do some awkwardness to get the exact ImageMagick command we
|
|
# want, though it's still less awkward than handling the command
|
|
# ourselves. Give all of the temporary images as input, flatten them and
|
|
# write them to the output path.
|
|
command = MiniMagick::CommandBuilder.new('convert')
|
|
temp_image_files.each { |image_file| command.push image_file.path }
|
|
command.layers 'flatten'
|
|
command.push output.path
|
|
|
|
# Though the above command really is sufficient, we still need a dummy
|
|
# image to handle execution.
|
|
output_image = MiniMagick::Image.new(output.path)
|
|
output_image.run(command)
|
|
|
|
temp_image_files.each(&:unlink)
|
|
else
|
|
output.close
|
|
end
|
|
end
|
|
|
|
def visible_assets
|
|
biology_assets = pet_state.swf_assets
|
|
object_assets = SwfAsset.object_assets.
|
|
fitting_body_id(pet_state.pet_type.body_id).for_item_ids(worn_item_ids)
|
|
|
|
# Now for fun with bitmasks! Rather than building a bunch of integer arrays
|
|
# here, we instead go low-level and use bit-level operations. Build the
|
|
# bitmask by parsing the binary string (reversing it to get the lower zone
|
|
# numbers on the right), then OR them all together to get the mask
|
|
# representing all the restricted zones. (Note to self: why not just store
|
|
# in this format in the first place?)
|
|
restrictors = biology_assets + worn_items
|
|
restricted_zones_mask = restrictors.inject(0) do |mask, restrictor|
|
|
mask | restrictor.zones_restrict.reverse.to_i(2)
|
|
end
|
|
|
|
# Now, check each asset's zone is not restricted in the bitmask using
|
|
# bitwise operations: shift 1 to the zone_id position, then AND it with
|
|
# the restricted zones mask. If we get 0, then the bit for that zone ID was
|
|
# not turned on, so the zone is not restricted and this asset is visible.
|
|
all_assets = biology_assets + object_assets
|
|
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)
|
|
bucket = SwfAsset::IMAGE_BUCKET
|
|
data = bucket.get(key)
|
|
file.binmode # write in binary mode
|
|
file.truncate(0) # clear the file
|
|
file.write data # write the new data
|
|
end
|
|
end
|
|
|