• Stars
    star
    114
  • Rank 308,031 (Top 7 %)
  • Language
    Ruby
  • License
    MIT License
  • Created over 10 years ago
  • Updated over 7 years ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

Explicit image processing based on Dragonfly.

Paperdragon

Explicit image processing.

Summary

Paperdragon gives you image processing as known from Paperclip, CarrierWave or Dragonfly. It allows uploading, cropping, resizing, watermarking, maintaining different versions of an image, and so on.

It provides a very explicit DSL: No magic is happening behind the scenes, paperdragon makes you implement the processing steps.

With only a little bit of more code you are fully in control of what gets uploaded where, which image version gets resized when and what gets sent to a background job.

Paperdragon uses the excellent Dragonfly gem for processing, resizing, storing, etc.

Paperdragon is database-agnostic, doesn't know anything about ActiveRecord and does not hook into AR's callbacks.

Installation

Add this line to your application's Gemfile:

gem 'paperdragon'

Example

This README only documents the public DSL. You're free to use the public API [documented here](# TODO) if you don't like the DSL.

Model

Paperdragon has only one requirement for the model: It needs to have a column image_meta_data. This is a serialised hash where paperdragon saves UIDs for the different image versions. We'll learn about this in a minute.

class User < ActiveRecord::Base # this could be just anything.
  include Paperdragon::Model

  processable :image

  serialize :image_meta_data
end

Calling ::processable advises paperdragon to create a User#image reader to the attachment. Nothing else is added to the class.

Uploading

Processing and storing an uploaded image is an explicit step - you have to code it! This code usually goes to a separate class or an Operation in Trailblazer, don't leave it in the controller if you don't have to.

def create
  file = params.delete(:image)

  user = User.create(params) # this is your code.

  # upload code:
  user.image(file) do |v|
    v.process!(:original)                                      # save the unprocessed.
    v.process!(:thumb)   { |job| job.thumb!("75x75#") }        # resizing.
    v.process!(:cropped) { |job| job.thumb!("140x140+20+20") } # cropping.
    v.process!(:public)  { |job| job.watermark! }              # watermark.
  end

  user.save
end

This is a completely transparent process.

  1. Calling #image usually returns the image attachment. However, passing a file to it allows to create different versions of the uploaded image in the block.
  2. #process! requires you to pass in a name for that particular image version. It is a convention to call the unprocessed image :original.
  3. The job object is responsible for creating the final version. This is simply a Dragonfly::Job object and gives you everything that can be done with dragonfly.
  4. After the block is run, paperdragon pushes a hash with all the images meta data to the model via model.image_meta_data=.

For a better understanding and to see how simple it is, go and check out the image_meta_data field.

 user.image_meta_data #=> {original: {uid: "original-logo.jpg", width: 240, height: 800},
                      #    thumb:    {uid: "thumb-logo.jpg", width: 140, height: 140},
                      #   ..and so on..
                      #   }

Rendering Images

After processing, you may want to render those image versions in your app.

user.image[:thumb].url

This is all you need to retrieve the URL/path for a stored image. Use this for your image tags.

= img_tag user.image[:thumb].url

Internally, Paperdragon will call model#image_meta_data and use this hash to find the address of the image.

While gems like paperclip often use several fields of the model to compute UIDs (addresses) at run-time, paperdragon does that once and then dumps it to the database. This completely removes the dependency to the model.

Reprocessing And Cropping

Once an image has been processed to several versions, you might need to reprocess some of them. As an example, users could re-crop their thumbs.

def crop
  user = User.find(params[:id]) # this is your code.

  # reprocessing code:
  cropping = "#{params[:w]}x#{params[:h]}#"

  user.image do |v|
    v.reprocess!(:thumb, Time.now) { |job| job.thumb!(cropping) } # re-crop.
  end

  user.save
end

Only a few things have changed compared to the initial processing.

  1. We do not pass a file to #image anymore. This makes sense as reprocessing will re-use the existing original file.
  2. Note that it's not #process! but #reprocess! indicating a surprising reprocessing.
  3. As a second argument to #reprocess! a fingerprint string is required. To understand what this does, let's inspect image_meta_data once again. (The fingerprint feature is optional but extremely helpful.)
 user.image_meta_data #   ..original..
                      #    thumb:    {uid: "thumb-logo-1234567890.jpg", width: 48, height: 48},
                      #   ..and so on..
                      #   }

See how the file name has changed? Paperdragon will automatically append the fingerprint you pass into #reprocess! to the existing version's file name.

Renaming

Sometimes you just want to rename files without processing them. For instance, when a new fingerprint for an image is introduced, you want to apply that to all versions.

fingerprint = Time.now

user.image do |v|
  v.reprocess!(:thumb, fingerprint) { |job| job.thumb!(cropping) } # re-crop.
  v.rename!(:original, fingerprint) # just rename it.
end

This will re-crop the thumb and rename the original.

 user.image_meta_data #=> {original: {uid: "original-logo-1234567890.jpg", ..},
                      #    thumb:    {uid: "thumb-logo-1234567890.jpg", ..},
                      #   ..and so on..
                      #   }

Deleting

While making images is a wonderful thing, sometimes you need to destroy to create. This is why paperdragon gives you a deleting mechanism, too.

user.image do |v|
  v.delete!(:thumb)
end

This will also remove the associated metadata from the model.

You can delete all versions of an attachment by omitting the style.

user.image do |v|
  v.delete! # deletes :original and :thumb.
end

Replacing Images

It's ok to run #process! again on a model with an existing attachment.

user.image_meta_data  #=> {original: {uid: "original-logo-1234567890.jpg", ..},

Processing here will overwrite the existing attachment.

user.image(new_file) do |v|
  v.process!(:original) # overwrites the existing, deletes old.
end
user.image_meta_data  #=> {original: {uid: "original-new-file01.jpg", ..},

While replacing the old with the new upload, the old file also gets deleted.

Fingerprints

Paperdragon comes with a very simple built-in file naming.

Computing a file UID (or, name, or path) happens in the Attachment class. You need to provide your own implementation if you want to change things.

class User < ActiveRecord::Base
  include Paperdragon::Model

  class Attachment < Paperdragon::Attachment
    def build_uid(style, file)
      "/path/to/#{style}/#{obfuscator}/#{file.name}"
    end

    def obfuscator
      Obfuscator.call # this is your code.
    end
  end

  processable :image, Attachment # use the class you just wrote.

The Attachment#build_uid method is invoked when processing images.

user.image(file) do |v|
  v.process!(:thumb)   { |job| job.thumb!("75x75#") }
end

To create the image UID, your attachment is now being used.

 user.image_meta_data #   ..original..
                      #    thumb:    {uid: "/path/to/thumb/ac97dnxid8/logo.jpg", ..},
                      #   ..and so on..
                      #   }

What a beautiful, cryptic and mysterious filename you just created!

The same pattern applies for re-building UIDs when reprocessing images.

class Attachment < Paperdragon::Attachment
  # def build_uid and the other code from above..

  def rebuild_uid(file, fingerprint)
    file.uid.sub("logo.png", "logo-#{fingerprint}.png")
  end
end

This code is used to re-compute UIDs in #reprocess!.

That example is stupid, I know, but it shows how you have access to the Paperdragon::File instance that represents the existing version of the reprocessed image.

Local Rails Configuration

Configuration of paperdragon completely relies on configuring dragonfly. As an example, for a Rails app with a local file storage, I use the following configuration in config/initializers/paperdragon.rb.

Dragonfly.app.configure do
  plugin :imagemagick

  datastore :file,
    :server_root => 'public',
    :root_path => 'public/images'
end

This would result in image UIDs being prefixed accordingly.

user.image[:thumb].url #=> "/images/logo-1234567890.png"

S3

As dragonfly allows S3, using the amazon cloud service is straight-forward.

All you need to do is configuring your bucket. The API for paperdragon remains unchanged.

require 'dragonfly/s3_data_store'

Dragonfly.app.configure do
  datastore :s3,
    bucket_name: 'my-bucket',
    access_key_id: 'blahblahblah',
    secret_access_key: 'blublublublu'
end

Images will be stored "in the cloud" when using #process!, renaming, deleting and re-processing do the same!

Background Processing

The explicit design of paperdragon makes it incredibly simple to move all or certain processing steps to background jobs.

class Image::Processor
  include Sidekiq::Worker

  def perform(params)
    user = User.find(params[:id])

    user.image(params[:file]) do |v|
      v.process!(:original)
    end
  end
end

Documentation how to use Sidekiq and paperdragon in Traiblazer will be added shortly.

Validations

Validating uploads are discussed in the Callbacks chapter of the Trailblazer book. We use file_validators.

Model: Reader and Writer

If you don't like Paperdragon::Model#image's fuzzy API you can use Reader and Writer.

The Writer will usually be mixed into a form.

class AlbumForm < Reform::Form
  extend Paperdragon::Model::Writer
  processable_writer :image

This provides the image! writer for processing a file.

form.image!(file) { |v| v.thumb!("64x64") }

Likewise, Reader will usually be used in cells or decorators.

class AlbumCell < Cell::ViewModel
  extend Paperdragon::Model::Reader
  processable_reader :image
  property :image_meta_data

You can now access the Attachment via image.

cell.image[:thumb].url

Paperclip compatibility

I wrote paperdragon as an explicit alternative to paperclip. In the process of doing so, I step-wise replaced upload code, but left the rendering code unchanged. Paperclip has a slightly different API for rendering.

user.image.url(:thumb)

Allowing your paperdragon-backed model to expose this API is piece-of-cake.

class User < ActiveRecord::Base
  include Paperdragon::Paperclip::Model

This will allow both APIs for a smooth transition.

More Repositories

1

apotomo

MVC Components for Rails.
Ruby
654
star
2

hooks

Generic hooks with callbacks for Ruby.
Ruby
281
star
3

disposable

Decorators on top of your ORM layer.
Ruby
171
star
4

gemgem-trbrb

The Trailblazer book's example app.
Ruby
137
star
5

uber

Gem-authoring extensions for classes and modules.
Ruby
94
star
6

cells_examples

A rails project to demonstrate cells in action.
Ruby
83
star
7

nit

Nit improves your git workflow.
Ruby
65
star
8

onfire

Have bubbling events and observers in all your Ruby objects.
Ruby
58
star
9

tyrant

Agnostic authentication gem with signup, signin, forgot password, sticky login, and so on.
Ruby
50
star
10

erbse

Next-generation ERB.
Ruby
48
star
11

kaminari-cells

Kaminari pagination in Cells.
Ruby
23
star
12

cells-blog-example

An exemplary Rails 3 Blog app using Cells.
JavaScript
22
star
13

pingback_engine

A Rails plugin for sending and receiving pingbacks.
Ruby
22
star
14

declarative

DSL for nested schemas.
Ruby
22
star
15

active_helper

Finally - helpers with proper encapsulation, delegation, interfaces and inheritance!
Ruby
19
star
16

gemgem

Sample Rails app using Trailblazer like a boss.com.au.
Ruby
18
star
17

rspec-apotomo

Spec your widgets.
Ruby
16
star
18

dashboard_tutorial

Rails app showing how to build dashboards with Apotomo.
Ruby
15
star
19

gemgem-hanami

Hanami with Trailblazer.
Ruby
14
star
20

cells-capture

Provides content_for for Cells.
Ruby
12
star
21

cells-sinatra

View Components for Sinatra.
Ruby
10
star
22

trb-cart

Shopping cart app with Trailblazer.
Ruby
10
star
23

declarative-option

Dynamic Options to evaluate at runtime.
Ruby
7
star
24

webmachinelovesroar

Ruby services to implement a RESTful shopping cart system with webmachine + roar.
Ruby
7
star
25

www.apotomo.de

Sources for the Apotomo project page, bundled as a Torture app (unreleased!).
Ruby
6
star
26

ruby-on-rest

The example apps from my" Ruby On REST" blog series.
Ruby
6
star
27

exp

My expense application, a Trailblazer+Sinatra example.
Ruby
6
star
28

torture

Tool collection to write and layout programmer's manuals.
Ruby
6
star
29

buildalib

The example app for the "Building your own authentication library with Trailblazer" book.
Ruby
6
star
30

gemgem-grape

HTTP API example application with Grape and Trailblazer.
Ruby
5
star
31

apotomo-stateful

Statefulness for your Apotomo widgets.
Ruby
5
star
32

cheatsheets

Cheatsheets for Cells, Apotomo and ROAR.
4
star
33

declarative-builder

Generic builder pattern.
Ruby
3
star
34

torture-server

Documentation framework
Ruby
3
star
35

pipetree

Functional nested pipeline dialect.
Ruby
3
star
36

screencastsofglory

The example project used for "Apotomo - Screencasts Of Glory"
Ruby
3
star
37

cells-filters

Before and after filters for your cells.
Ruby
3
star
38

cells-widget

Cells view models that can be updated via AJAX.
2
star
39

sinatra-on-cells

Sinatra app using Rails cells and cool Rails gems like simple_form.
Ruby
2
star
40

trb-rubyday

Where's my limoncello?
Ruby
2
star
41

cells-examples

Examples of using Cells with plain Ruby, Lotus, Sinatra, and more. Includes caching, view inheritance, etc.
Ruby
1
star
42

subliminal

My Sublime 3 configuration and snippets.
1
star
43

roar-form_json

Representers for the (unofficial) Form+JSON media type.
Ruby
1
star