Dragonfly

Image/asset management for winners

GitHub Repository

Models

Defining accessors

Any class can be extended to add Dragonfly attachment functionality.

Let’s say you have a model with methods image_uid and image_uid=

class Photo
  attr_accessor :image_uid
end

Then you can add the Dragonfly attachment methods image and image= with

class Photo
  extend Dragonfly::Model
  dragonfly_accessor :image
end

To define an accessor using a named (i.e. non-default) Dragonfly app such as

Dragonfly.app(:assets)

you can pass a the app name as an option

  dragonfly_accessor :image, :app => :assets

If the model class responds to before_save and before_destroy (like with ActiveRecord) then the attachment will be stored on save (when changed) and destroyed on destroy.

To extend all ActiveRecord models, you can do

ActiveRecord::Base.extend Dragonfly::Model

Using the accessors

We can use the attribute much like other other model attributes:

@photo = Photo.new

@photo.image = "\377???JFIF\000\..."             # can assign as a string...
@photo.image = File.new('path/to/my_image.png')  # ... or as a file...
@photo.image = some_tempfile                     # ... or as a tempfile...
@photo.image = Pathname.new('some/path.gif')     # ... or as a pathname...
@photo.image = @photo.band_photo                 # ... or as another Dragonfly attachment

@photo.image          # => <Dragonfly Attachment uid=nil, app=:default>

When setting with a string you should also set a name so that Dragonfly knows its mime-type

@photo.image = "\377???JFIF\000\..."
@photo.image.name = "file.png"
@photo.image.mime_type     # ===> "image/png"

Set to nil to remove it

@photo.image = nil
@photo.image          # => nil

We can inspect properties of the attribute

@photo.image.mime_type                      # => 'image/png'
@photo.image.size                           # => 63425 (size in bytes)

We can add analyser methods (see Analysers), e.g. the ImageMagick plugin adds

@photo.image.width                          # => 280
@photo.image.height                         # => 140

We can play around with the data

@photo.image.data                           # => "\377???JFIF\000\..."
@photo.image.to_file('out.png')

see How does it work? - Using the content for more examples (Attachment objects work in a similar way to Job objects).

We can add processor methods (see Processors), e.g. the ImageMagick plugin adds

image = @photo.image.thumb('20x20')   # returns a 'Job' object
image.width                           # =>  20

image = @photo.image.encode('gif')
image.format                          # => 'gif'

Assigning from a URL

Dragonfly provides an accessor for assigning directly from a url:

@photo.image_url = 'http://some.url/file.jpg'

You can put this in a form view, e.g. in rails erb:

<%= f.text_field :image_url %>

It also works for data uris

@photo.image_url = "data:image/jpeg;base64,GEGseg42g3g..."

Removing an attachment via a form

Normally unassignment of an attachment is done like any other attribute, by setting to nil

@photo.image = nil

but this can’t be done via a form - instead remove_<attachment_name> is provided, which can be used with a checkbox:

<%= f.check_box :remove_image %>

Retaining across form redisplays

When a model fails validation, you don’t normally want to have to upload your attachment again, so you can avoid having to do this by including a hidden field in your form retained_<attribute_name>, e.g.

<% form_for @photo, :html => {:multipart => true} do |f| %>
  ...
  <%= f.file_field :image %>
  <%= f.hidden_field :retained_image %>
  ...
<% end %>

Persisting

When the model is saved, a before_save callback persists the data to the Dragonfly app’s configured datastore. The uid column is then filled in.

@photo = Photo.new

@photo.image_uid     # => nil

@photo.image = File.new('path/to/my_image.png')
@photo.image_uid     # => nil

@photo.save
@photo.image_uid     # => '2009/12/05/file.png' (some unique uid, used by the datastore)

URLs

Once the model is saved, we can get a url for the image which will be served by Dragonfly, and for its processed versions.

@photo.image.url                           # => '/media/W1siZyIsInRleHQ...'
@photo.image.thumb('300x200#nw').url       # => '/media/WgsdoicjdslliZy...'
@photo.image.encode('gif').url             # => '/media/Wiflkpiubppndsh...'

Because the processing methods are lazy, no processing is actually done in the above code.

Reflection methods

xxx_stored? tells you if the content has been stored in the data store. It’s useful for deciding whether the url is available or not

<%= image_tag @photo.image.thumb('20x30').url if @photo.image_stored? %>

xxx_changed? tells you if the content has been changed since saving. It’s useful for deciding whether to validate or not

@photo.image_changed?  # ===> false
@photo.image = some_file
@photo.image_changed?  # ===> true

Validations

validates_presence_of and validates_size_of work out of the box, and Dragonfly also provides validates_property.

class Photo
  extend Dragonfly::Model::Validations

  validates_presence_of :image
  validates_size_of :image, maximum: 500.kilobytes

  # Check the file extension
  validates_property :ext, of: :image, as: 'jpg'
  # ..or..
  validates_property :mime_type, of: :image, as: 'image/jpeg'
  # ..or actually analyse the format with imagemagick..
  validates_property :format, of: :image, in: ['jpeg', 'png', 'gif']

  validates_property :width, of: :image, in: (0..400), message: "é demais cara!"

  # ..or you might want to use image_changed? method..
  validates_property :format, of: :image, as: 'png', if: :image_changed?

  # ...
end

validates_property will work for any property, not just Dragonfly analysers, because internally it simply calls send on the attribute.

It does nothing if the accessor is not set (i.e. returns nil).

It can also take a proc for the message

validates_property :width, of: :image, in: (0..400),
                           message: proc{|actual, model| "Unlucky #{model.title} - was #{actual}" }

Name and extension

If the object assigned is a file, or responds to original_filename (as is the case with file uploads in Rails, etc.), then name will be set.

@photo.image = File.new('path/to/my_image.png')

@photo.image.name    # => 'my_image.png'
@photo.image.ext     # => 'png'

Meta data

You can store metadata along with the content data of your attachment:

@photo.image = File.new('path/to/my_image.png')
@photo.image.meta['taken'] = Date.yesterday.to_s
@photo.save!

@photo.reload.image.meta['taken']      # ==> '2018-04-05'

NOTE meta must be serializable to/from JSON, i.e. consist of strings, numbers, booleans, NOT symbols, dates, etc.

Meta data can be useful because at the time that Dragonfly serves content, it doesn’t have access to your model, but it does have access to the meta data that was stored alongside the content, so you could use it to provide custom response headers, etc.

Default content

Normally if an accessor is not set it returns nil

@photo.image   # ===> nil

However we can get it to return something by default by giving a path

class Photo
  dragonfly_accessor :image do
    default 'public/images/default.png'
  end
end
@photo.image      # ===> Job object
@photo.image.url  # ===> "/WsDf32g...."

the Job object returned is equivalent to

Dragonfly.app.fetch_url('public/images/default.png')

Callbacks

after_assign

after_assign can be used to do something every time content is assigned:

class Person
  dragonfly_accessor :mugshot do
    after_assign{|a| a.rotate!(90) }  # 'a' is the attachment itself
  end
end

person.mugshot = Pathname.new('some/path.png')  # after_assign callback is called
person.mugshot = nil                            # after_assign callback is NOT called

Inside the block, you can call methods on the model instance directly (self is the model):

class Person
  dragonfly_accessor :mugshot do
    after_assign{|a| a.rotate!(angle) }
  end

  def angle
    90
  end
end

Alternatively you can pass in a symbol, corresponding to a model instance method:

class Person
  dragonfly_accessor :mugshot do
    after_assign :rotate_it
  end

  def rotate_it
    mugshot.rotate!(90)
  end
end

You can register more than one after_assign callback.

after_unassign

after_unassign is similar to after_assign, but is only called when the attachment is unassigned

person.mugshot = Pathname.new('some/path.png')  # after_unassign callback is NOT called
person.mugshot = nil                            # after_unassign callback is called

Up-front processing

The best way to create different versions of content such as thumbnails is generally on-the-fly, however if you must create another version on-upload, then you could create another accessor and automatically copy to it using copy_to.

class Person
  dragonfly_accessor :mugshot do
    copy_to(:smaller_mugshot){|a| a.thumb('200x200#') }
  end
  dragonfly_accessor :smaller_mugshot
end

person.mugshot = Pathname.new('some/400x300/image.png')

person.mugshot            # ---> 400x300 image
person.smaller_mugshot    # ---> 200x200 image

In the above example you would need both a mugshot_uid field and a smaller_mugshot_uid field on your model.

You can also do this manually (e.g. in a background task) using

person.smaller_mugshot = person.mugshot.thumb('200x200#')
person.save

Storage options

Some datastores take options when calling store - you can pass these through with storage_options.

For example, the file datastore takes a :path option to specify where to store the content (which will also become the uid for that content).

class Person
  dragonfly_accessor :mugshot do
    storage_options do |a|
      # self is the model and a is the attachment
      { path: "some/path/#{self.category}-#{a.width}" }
    end
  end
end

or

class Person
  dragonfly_accessor :mugshot do
    storage_options :opts_for_storage
  end

  def opts_for_storage
    { path: "some/path/#{category}/#{rand(100)}" }
  end
end

BEWARE!!!! you must make sure the path (which will become the uid for the content) is unique and changes each time the content is changed, otherwise you could have caching problems, as the generated urls will be the same for the same uid.

BEWARE No. 2!!!! using id in the storage_path won’t generally work on create, because Dragonfly stores the content in a call to before_save, at which point the id won’t yet exist.

"Magic" Attributes

An accessor like image only relies on the accessor image_uid to work. However, in some cases you may want to record some other properties, whether it be for using in queries, or for caching an attribute for performance reasons, etc.

For the properties name, size and any of the registered analysis methods (e.g. width), this is done automatically for you, if the corresponding accessor exists.

For example - with ActiveRecord, given the migration:

add_column :photos, :image_width, :integer

This will automatically be set when assigned:

@photo.image = File.new('path/to/my_image.png')

@photo.image_width  # => 280

They can be used to avoid retrieving data from the datastore for analysis

@photo = Photo.first

@photo.image.width     # => 280    - no need to retrieve data - takes it from `image_width`
@photo.image.size      # => 134507 - but this needs to retrieve data from the data store, then analyse

Furthermore, any magic attributes you add a field for will be added to the meta data for that attachment, so can be used to set custom response headers when Dragonfly serves the content.