cropped_paperclip 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
+