cropped_paperclip 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (77) hide show
  1. data/.gitignore +8 -0
  2. data/.rspec +2 -0
  3. data/.rvmrc +48 -0
  4. data/Gemfile +7 -0
  5. data/README.md +17 -0
  6. data/Rakefile +31 -0
  7. data/app/.DS_Store +0 -0
  8. data/app/assets/.DS_Store +0 -0
  9. data/app/assets/javascripts/.DS_Store +0 -0
  10. data/app/assets/javascripts/cropped_paperclip/filedrop.js.coffee +254 -0
  11. data/app/assets/javascripts/cropped_paperclip/upload_crop_scale.js.coffee +319 -0
  12. data/app/assets/javascripts/es5-shim.js +1105 -0
  13. data/app/assets/javascripts/uploader.js.coffee +3 -0
  14. data/app/controllers/cropped_paperclip/application_controller.rb +4 -0
  15. data/app/controllers/cropped_paperclip/uploads_controller.rb +40 -0
  16. data/app/helpers/cropped_paperclip/application_helper.rb +4 -0
  17. data/app/models/upload.rb +119 -0
  18. data/config/routes.rb +3 -0
  19. data/cropped_paperclip.gemspec +32 -0
  20. data/db/migrate/20120510103921_uploads.rb +11 -0
  21. data/init.rb +4 -0
  22. data/lib/cropped_paperclip.rb +170 -0
  23. data/lib/cropped_paperclip/engine.rb +7 -0
  24. data/lib/cropped_paperclip/glue.rb +20 -0
  25. data/lib/cropped_paperclip/schema.rb +35 -0
  26. data/lib/cropped_paperclip/version.rb +3 -0
  27. data/lib/paperclip/geometry_transformation.rb +80 -0
  28. data/lib/paperclip/validators/attachment_height_validator.rb +89 -0
  29. data/lib/paperclip/validators/attachment_width_validator.rb +89 -0
  30. data/lib/paperclip_processors/offset_thumbnail.rb +86 -0
  31. data/lib/tasks/cropped_paperclip_tasks.rake +4 -0
  32. data/script/rails +8 -0
  33. data/spec/.DS_Store +0 -0
  34. data/spec/acceptance/acceptance_helper.rb +2 -0
  35. data/spec/controllers/uploads_controller_spec.rb +5 -0
  36. data/spec/dummy/README.rdoc +261 -0
  37. data/spec/dummy/Rakefile +7 -0
  38. data/spec/dummy/app/assets/javascripts/application.js +15 -0
  39. data/spec/dummy/app/assets/stylesheets/application.css +13 -0
  40. data/spec/dummy/app/controllers/application_controller.rb +3 -0
  41. data/spec/dummy/app/helpers/application_helper.rb +2 -0
  42. data/spec/dummy/app/mailers/.gitkeep +0 -0
  43. data/spec/dummy/app/models/.gitkeep +0 -0
  44. data/spec/dummy/app/models/thing.rb +15 -0
  45. data/spec/dummy/app/views/layouts/application.html.erb +14 -0
  46. data/spec/dummy/config.ru +4 -0
  47. data/spec/dummy/config/application.rb +56 -0
  48. data/spec/dummy/config/boot.rb +10 -0
  49. data/spec/dummy/config/database.yml +25 -0
  50. data/spec/dummy/config/environment.rb +5 -0
  51. data/spec/dummy/config/environments/development.rb +37 -0
  52. data/spec/dummy/config/environments/production.rb +67 -0
  53. data/spec/dummy/config/environments/test.rb +37 -0
  54. data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
  55. data/spec/dummy/config/initializers/inflections.rb +15 -0
  56. data/spec/dummy/config/initializers/mime_types.rb +5 -0
  57. data/spec/dummy/config/initializers/secret_token.rb +7 -0
  58. data/spec/dummy/config/initializers/session_store.rb +8 -0
  59. data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
  60. data/spec/dummy/config/locales/en.yml +5 -0
  61. data/spec/dummy/config/routes.rb +3 -0
  62. data/spec/dummy/db/migrate/20120510104910_things.rb +8 -0
  63. data/spec/dummy/db/schema.rb +51 -0
  64. data/spec/dummy/lib/assets/.gitkeep +0 -0
  65. data/spec/dummy/log/.gitkeep +0 -0
  66. data/spec/dummy/public/404.html +26 -0
  67. data/spec/dummy/public/422.html +26 -0
  68. data/spec/dummy/public/500.html +25 -0
  69. data/spec/dummy/public/favicon.ico +0 -0
  70. data/spec/dummy/script/rails +6 -0
  71. data/spec/fixtures/images/.DS_Store +0 -0
  72. data/spec/fixtures/images/icon.png +0 -0
  73. data/spec/fixtures/images/test.jpg +0 -0
  74. data/spec/models/thing_spec.rb +6 -0
  75. data/spec/models/upload_spec.rb +13 -0
  76. data/spec/spec_helper.rb +18 -0
  77. metadata +309 -0
@@ -0,0 +1,3 @@
1
+ #= require es5-shim
2
+ #= require cropped_paperclip/filedrop
3
+ #= require cropped_paperclip/upload_crop_scale
@@ -0,0 +1,4 @@
1
+ module CroppedPaperclip
2
+ class ApplicationController < ActionController::Base
3
+ end
4
+ end
@@ -0,0 +1,40 @@
1
+ module CroppedPaperclip
2
+ class UploadsController < ::ApplicationController
3
+ respond_to :js
4
+ before_filter :find_upload, :only => [:show, :edit, :destroy]
5
+ before_filter :build_upload, :only => [:new, :create]
6
+
7
+ def show
8
+ respond_with(@upload)
9
+ end
10
+
11
+ def new
12
+ render
13
+ end
14
+
15
+ def create
16
+ @upload.update_attributes(params[:upload])
17
+ render :partial => 'crop'
18
+ end
19
+
20
+ def edit
21
+ render :partial => 'crop'
22
+ end
23
+
24
+ def destroy
25
+ @upload.destroy
26
+ respond_with(@upload)
27
+ end
28
+
29
+ private
30
+
31
+ def find_upload
32
+ @upload = params[:id] == 'latest' || params[:id].blank? ? current_user.last_upload : Upload.find(params[:id])
33
+ end
34
+
35
+ def build_upload
36
+ @upload = Upload.create
37
+ end
38
+
39
+ end
40
+ end
@@ -0,0 +1,4 @@
1
+ module CroppedPaperclip
2
+ module ApplicationHelper
3
+ end
4
+ end
@@ -0,0 +1,119 @@
1
+ # This is a standard upload class that should be useable for most purposes.
2
+ # We assume that even when the final destination is an S3 bucket, the initial upload
3
+ # will be held locally.
4
+ #
5
+ class Upload < ActiveRecord::Base
6
+ has_attached_file :file,
7
+ :path => ":rails_root/public/system/:class/:attachment/:id/:style/:filename",
8
+ :url => "/system/:class/:attachment/:id/:style/:filename",
9
+ :processors => lambda { |instance| instance.precrop_processors },
10
+ :styles => lambda { |attachment| attachment.instance.precrop_styles }
11
+
12
+ attr_accessible :file
13
+
14
+ # To change precrop dimensions or other thumbnail properties, just monkeypatch this method.
15
+ #
16
+ def precrop_styles
17
+ {
18
+ :icon => { :geometry => "40x40#" },
19
+ :thumb => { :geometry => "100x100#" },
20
+ :precrop => { :geometry => "1600x3000" }
21
+ }
22
+ end
23
+
24
+ def precrop_processors
25
+ [:thumbnail]
26
+ end
27
+
28
+ validates :file, :attachment_presence => true
29
+
30
+ ## Image dimensions
31
+ #
32
+ # We need to know dimensions of the precrop image in order to set up the cropping interface, so we
33
+ # examine the uploaded file before it is flushed.
34
+ #
35
+ after_post_process :read_dimensions
36
+
37
+ # *original_geometry* returns the discovered dimensions of the uploaded file as a paperclip geometry object.
38
+ #
39
+ def original_geometry
40
+ @original_geometry ||= Paperclip::Geometry.new(original_width, original_height)
41
+ end
42
+
43
+ # *geometry*, given a style name, returns the dimensions of the file if that style were applied. For
44
+ # speed we calculate this rather than reading the file, which might be in S3 or some other distant place.
45
+ #
46
+ # The logic is in [lib/paperclip/geometry_tranformation.rb](/lib/paperclip/geometry_tranformation.html),
47
+ # which is a ruby library that mimics the action of imagemagick's convert command.
48
+ #
49
+ def geometry(style_name='original')
50
+ # These calculations are all memoised.
51
+ @geometry ||= {}
52
+ begin
53
+ @geometry[style_name] ||= if style_name.to_s == 'original'
54
+ # If no style name is given, or it is 'original', we return the original discovered dimensions.
55
+ original_geometry
56
+ else
57
+ # Otherwise, we apply a mock transformation to see what dimensions would result.
58
+ style = self.file.styles[style_name.to_sym]
59
+ original_geometry.transformed_by(style.geometry)
60
+ end
61
+ rescue Paperclip::TransformationError => e
62
+ # In case of explosion, we always return the original dimensions so that action can continue.
63
+ original_geometry
64
+ end
65
+ end
66
+
67
+ # *width* returns the width of this image in a given style.
68
+ #
69
+ def width(style_name='original')
70
+ geometry(style_name).width.to_i
71
+ end
72
+
73
+ # *height* returns the height of this image in a given style.
74
+ #
75
+ def height(style_name='original')
76
+ geometry(style_name).height.to_i
77
+ end
78
+
79
+ # *square?* returns true if width and height are the same.
80
+ #
81
+ def square?(style_name='original')
82
+ geometry(style_name).square?
83
+ end
84
+
85
+ # *vertical?* returns true if the image, in the given style, is taller than it is wide.
86
+ #
87
+ def vertical?(style_name='original')
88
+ geometry(style_name).vertical?
89
+ end
90
+
91
+ # *horizontal?* returns true if the image, in the given style, is wider than it is tall.
92
+ #
93
+ def horizontal?(style_name='original')
94
+ geometry(style_name).horizontal?
95
+ end
96
+
97
+ # *dimensions_known?* returns true we have managed to discover the dimensions of the original file.
98
+ #
99
+ def dimensions_known?
100
+ original_width? && original_height?
101
+ end
102
+
103
+ private
104
+
105
+ # *read_dimensions* is called after post processing to record in the database the original width, height
106
+ # and extension of the uploaded file. At this point the file queue will not have been flushed but the upload
107
+ # should be in place. We grab dimensions from the temp file and calculate thumbnail dimensions later, on demand.
108
+ #
109
+ def read_dimensions
110
+ if file = self.file.queued_for_write[:original]
111
+ geometry = Paperclip::Geometry.from_file(file)
112
+ self.original_width = geometry.width
113
+ self.original_height = geometry.height
114
+ self.original_extension = File.extname(file.path)
115
+ end
116
+ true
117
+ end
118
+
119
+ end
data/config/routes.rb ADDED
@@ -0,0 +1,3 @@
1
+ Rails.application.routes.draw do
2
+ resources :uploads, :controller => 'cropped_paperclip/uploads'
3
+ end
@@ -0,0 +1,32 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "cropped_paperclip/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "cropped_paperclip"
7
+ s.version = CroppedPaperclip::VERSION
8
+ s.authors = ["William Ross"]
9
+ s.email = ["will@spanner.org"]
10
+ s.homepage = ""
11
+ s.summary = %q{A simple but specific way to attach croppable uploads to any model}
12
+ s.description = %q{Provides a mechanism for uploading, cropping and reusing images in any of your models.}
13
+
14
+ s.rubyforge_project = "cropped_paperclip"
15
+
16
+ s.files = `git ls-files`.split("\n")
17
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
18
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
19
+ s.require_paths = ["lib"]
20
+
21
+ s.add_dependency "rails", "~> 3.2.0"
22
+ s.add_dependency('paperclip', '~> 3.1.0')
23
+ s.add_dependency('delayed_job_active_record')
24
+
25
+ s.add_development_dependency "rake"
26
+ s.add_development_dependency "rspec-rails"
27
+ s.add_development_dependency "shoulda-matchers"
28
+ s.add_development_dependency "capybara"
29
+ s.add_development_dependency "acts_as_fu"
30
+ s.add_development_dependency "sqlite3"
31
+
32
+ end
@@ -0,0 +1,11 @@
1
+ class Uploads < ActiveRecord::Migration
2
+ def change
3
+ create_table :uploads do |t|
4
+ t.has_attached_file :file
5
+ t.string :original_extension
6
+ t.integer :original_width
7
+ t.integer :original_height
8
+ t.timestamps
9
+ end
10
+ end
11
+ end
data/init.rb ADDED
@@ -0,0 +1,4 @@
1
+ require File.join(File.dirname(__FILE__), "lib", "cropped_paperclip")
2
+ require 'cropped_paperclip/railtie'
3
+
4
+ CroppedPaperclip::Railtie.insert
@@ -0,0 +1,170 @@
1
+ require "paperclip"
2
+ require "paperclip/validators/attachment_height_validator"
3
+ require "paperclip/validators/attachment_width_validator"
4
+ require "paperclip/geometry_transformation"
5
+ require "paperclip_processors/offset_thumbnail"
6
+ require "cropped_paperclip/engine"
7
+ require 'cropped_paperclip/glue'
8
+
9
+ module CroppedPaperclip
10
+ mattr_accessor :attachment_path
11
+ mattr_accessor :attachment_url
12
+ attachment_path = ":rails_root/public/system/:class/:attachment/:id/:style/:filename"
13
+ attachment_url = "/system/:class/:attachment/:id/:style/:filename"
14
+
15
+ # CroppedPaperclip::ClassMethods is included into ActiveRecord::Base in the same way as the Paperclip module.
16
+ # It adds a `has_upload` class method that defines an attachment and adds several instance methods
17
+ # that will return the values that determine its cropping. Those values are usually but not
18
+ # necessarily given by the user.
19
+ #
20
+
21
+ module ClassMethods
22
+ ## Defining upload columns
23
+ #
24
+ # *has_upload* brings in the whole machinery of receiving and cropping an uploaded file. eg.
25
+ #
26
+ # class User < ActiveRecord::Base
27
+ # has_upload :avatar, :size => '120x120#'
28
+ #
29
+ # The geometry string will always be treated as though it ended in '#'.
30
+ #
31
+ # Set the :cropped option to false if you want the file-upload-sharing mechanism but no cropping step.
32
+ # In that case any geometry string can be used and it will be passed through intact.
33
+ #
34
+ # class Group < ActiveRecord::Base
35
+ # has_upload :icon, :size => '40x40#', :crop => false
36
+ #
37
+ def has_upload(attachment_name=:image, options={})
38
+ unless !table_exists? || column_names.include?("#{attachment_name}_upload_id")
39
+ raise RuntimeError, "has_upload(#{attachment_name}) called on class #{self.to_s} but we have no #{attachment_name}_upload_id column"
40
+ end
41
+
42
+ options.reverse_merge!(:geometry => "640x960#", :cropped => true, :whiny => true)
43
+ options[:geometry].sub!(/\D*$/, '') if options[:cropped]
44
+ # raise here if geometry is not useable
45
+
46
+ class_variable_set(:"@@#{attachment_name}_cropped", options[:cropped])
47
+
48
+ # The essential step is present in this style definition. It specifies the OffsetThumbnail processor,
49
+ # which is similar to the usual thumbnailer but has a more flexible scaling and cropping procedure,
50
+ # and passes through a couple of callback procs that will return the scaling and cropping arguments
51
+ # it requires.
52
+ #
53
+ crop_style = options[:cropped] == false ? geometry : {
54
+ :geometry => "#{options[:geometry]}#",
55
+ :processors => [:offset_thumbnail],
56
+
57
+ # The processor will first scale the image to the width that is specified by the scale_width property of the instance
58
+ :scale => lambda { |att|
59
+ width = att.instance.send :"#{attachment_name}_scale_width"
60
+ "#{width || 0}x"
61
+ },
62
+
63
+ # ...then perform the crop described by the width, height, offset_top and offset_left properties of the instance.
64
+ :crop_and_offset => lambda { |att|
65
+ width, height = options[:geometry].split('x')
66
+ left = att.instance.send :"#{attachment_name}_offset_left" || 0
67
+ top = att.instance.send :"#{attachment_name}_offset_top"
68
+ "%dx%d%+d%+d" % [width, height, -(left || 0), -(top || 0)]
69
+ }
70
+ }
71
+
72
+ options[:styles] ||= { :icon => "48x48#" }
73
+ options[:styles].merge!({:cropped => crop_style})
74
+
75
+ ### Upload association
76
+ #
77
+ # [uploads](/app/models/upload.html) are the raw image files uploaded by this person.
78
+ # They are held separately as the basis for repeatable (and shareable) image assignment.
79
+ #
80
+ belongs_to :"#{attachment_name}_upload", :class_name => "Upload"
81
+ before_save :"read_#{attachment_name}_upload"
82
+
83
+ ### Attachment
84
+ #
85
+ # Image attachments work in the usual Paperclip way except that the :cropped style is applied differently to each instance.
86
+ # The editing interface allows the user to upload a picture (which creates an upload object) and choose how it is scaled
87
+ # and cropped (which stores values here).
88
+ #
89
+ # The cropped image is created by a [custom processor](/lib/paperclip_processors/offset_thumbnail.html) very similar to
90
+ # Paperclip::Thumbnail, but which looks up the scale and crop parameters to calculate the imagemagick transformation.
91
+ #
92
+ has_attached_file attachment_name, options
93
+
94
+ ## Maintenance
95
+ #
96
+ # *read_[name]_upload* is called before_save. If there is a new upload, or any of our scale and crop values are changed, it will assign
97
+ # the uploaded file. Even if it's the same file as before, the effect is to trigger post-processing again and apply the current crop and scale values.
98
+ #
99
+ define_method :"read_#{attachment_name}_upload" do
100
+ if self.send(:"reprocess_#{attachment_name}?") && upload = self.send(:"#{attachment_name}_upload")
101
+ self.send :"#{attachment_name}=", upload.file
102
+ end
103
+ end
104
+
105
+ # *reprocess_[name]?* returns true if there have been any changes to the upload association that would necessitate a new crop.
106
+ #
107
+ cols = [:upload_id]
108
+ cols += [:upload_id, :scale_width, :offset_top, :offset_left] if options[:cropped]
109
+ define_method :"reprocess_#{attachment_name}?" do
110
+ cols.any? {|col| send(:"#{attachment_name}_#{col}_changed?") }
111
+ end
112
+
113
+ # * [name]_cropped? returns true if the named attachment is cropped on assignment. It can be useful in a form partial.
114
+ #
115
+ define_method :"#{attachment_name}_cropped?" do
116
+ STDERR.puts ">> #{attachment_name}_cropped?"
117
+ !!class_variable_get(:"@@#{attachment_name}_cropped")
118
+ end
119
+
120
+ define_method :"#{attachment_name}_for_cropping" do
121
+ if upload = send(:"#{attachment_name}_upload")
122
+ # here we introduce a convention that might not stand up
123
+ STDERR.puts ">> #{attachment_name}_for_cropping"
124
+ upload.url(:"#{attachment_name}")
125
+ end
126
+ end
127
+
128
+ end
129
+
130
+ ## Delay post-processing
131
+
132
+ def delay_post_processing(attachment_name=:image)
133
+ send(:"before_#{attachment_name}_post_process", :"defer_#{attachment_name}_post_processing")
134
+ after_save(:"resume_#{attachment_name}_post_processing")
135
+
136
+ # There are too many thumbnail styles in this class. We can't make the user wait while they are processed,
137
+ # so the whole job of thumbnailing is spun off into a delayed_job. Since the main publication page displays
138
+ # the :original style, we can show the user her public page while the rest of the thumbnails are still being
139
+ # processed.
140
+ #
141
+ # The usual post_processing routine is abandoned when we return false from this call.
142
+ #
143
+ define_method :"defer_#{attachment_name}_post_processing" do
144
+ if send(:"reprocess_#{attachment_name}?") && !send(:"awaiting_#{attachment_name}_processing?")
145
+ send(:"awaiting_#{attachment_name}_processing", true)
146
+ false
147
+ end
148
+ end
149
+
150
+ # The delayed job is created just by interposing the `delay` method in a call to `process_image_styles!`. The effect
151
+ # is to serialize this object and that call to the database and resume it later when the job runner picks it up.
152
+ # We can't do that until the publication object has an id, so the call is made from an after_save handler.
153
+ #
154
+ define_method :"resume_#{attachment_name}_post_processing" do
155
+ if send(:"reprocess_#{attachment_name}?") && send(:"awaiting_#{attachment_name}_processing?")
156
+ self.delay.send(:"process_#{attachment_name}_styles!")
157
+ end
158
+ end
159
+
160
+ # This is the eventual processing step, to which the delayed job object is just a sort of pointer.
161
+ # It retrieves the original image from S3 and applies the processing styles.
162
+ #
163
+ define_method :"process_#{attachment_name}_styles!" do
164
+ send(attachment_name).reprocess!
165
+ update_column(:"awaiting_#{attachment_name}_processing", false)
166
+ end
167
+
168
+ end
169
+ end
170
+ end
@@ -0,0 +1,7 @@
1
+ module CroppedPaperclip
2
+ class Engine < Rails::Engine
3
+ initializer "cropped_paperclip.integration" do
4
+ ActiveRecord::Base.send(:include, CroppedPaperclip::Glue)
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,20 @@
1
+ require 'cropped_paperclip/schema'
2
+
3
+ module CroppedPaperclip
4
+ module Glue
5
+ def self.included base #:nodoc:
6
+
7
+ # Extend ActiveRecord::Base with CroppedPaperclip::ClassMethods, as defined in cropped_paperclip.rb.
8
+ #
9
+ base.extend ClassMethods
10
+
11
+ # Load migration helpers into all the right places.
12
+ #
13
+ if defined?(ActiveRecord)
14
+ ActiveRecord::ConnectionAdapters::AbstractAdapter.send(:include, CroppedPaperclip::Schema)
15
+ ActiveRecord::ConnectionAdapters::Table.send(:include, CroppedPaperclip::Schema)
16
+ ActiveRecord::ConnectionAdapters::TableDefinition.send(:include, CroppedPaperclip::Schema)
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,35 @@
1
+ module CroppedPaperclip
2
+ # Provides helpers that can be used in migrations.
3
+ # Copied from, and often makes calls on, the equivalent file in Paperclip.
4
+ #
5
+ module Schema
6
+ UPLOAD_COLUMNS = {
7
+ :file_name => :string,
8
+ :content_type => :string,
9
+ :file_size => :integer,
10
+ :updated_at => :datetime,
11
+ :upload_id => :integer,
12
+ :scale_width => :integer,
13
+ :scale_height => :integer,
14
+ :offset_left => :integer,
15
+ :offset_top => :integer
16
+ }
17
+
18
+ def self.included(base)
19
+ ActiveRecord::ConnectionAdapters::Table.send :include, TableDefinition
20
+ ActiveRecord::ConnectionAdapters::TableDefinition.send :include, TableDefinition
21
+ end
22
+
23
+ module TableDefinition
24
+ def uploadable_attachment(*attachment_names)
25
+ attachment_names.each do |attachment_name|
26
+ UPLOAD_COLUMNS.each_pair do |column_name, column_type|
27
+ column("#{attachment_name}_#{column_name}", column_type)
28
+ end
29
+ end
30
+ end
31
+ end
32
+
33
+ end
34
+ end
35
+