paperclip 3.0.3 → 3.5.1

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of paperclip might be problematic. Click here for more details.

Files changed (121) hide show
  1. checksums.yaml +15 -0
  2. data/.gitignore +2 -1
  3. data/.travis.yml +3 -0
  4. data/Appraisals +8 -3
  5. data/Gemfile +1 -1
  6. data/LICENSE +1 -1
  7. data/NEWS +198 -35
  8. data/README.md +332 -113
  9. data/features/basic_integration.feature +24 -12
  10. data/features/migration.feature +94 -0
  11. data/features/rake_tasks.feature +2 -3
  12. data/features/step_definitions/attachment_steps.rb +28 -0
  13. data/features/step_definitions/rails_steps.rb +94 -8
  14. data/features/step_definitions/s3_steps.rb +1 -1
  15. data/features/step_definitions/web_steps.rb +3 -3
  16. data/features/support/fakeweb.rb +4 -1
  17. data/features/support/file_helpers.rb +10 -0
  18. data/features/support/rails.rb +18 -2
  19. data/gemfiles/3.0.gemfile +2 -2
  20. data/gemfiles/3.1.gemfile +2 -2
  21. data/gemfiles/3.2.gemfile +2 -2
  22. data/gemfiles/4.0.gemfile +11 -0
  23. data/lib/generators/paperclip/templates/paperclip_migration.rb.erb +4 -8
  24. data/lib/paperclip/attachment.rb +96 -43
  25. data/lib/paperclip/attachment_registry.rb +57 -0
  26. data/lib/paperclip/callbacks.rb +2 -2
  27. data/lib/paperclip/content_type_detector.rb +78 -0
  28. data/lib/paperclip/file_command_content_type_detector.rb +32 -0
  29. data/lib/paperclip/filename_cleaner.rb +16 -0
  30. data/lib/paperclip/geometry.rb +66 -30
  31. data/lib/paperclip/geometry_detector_factory.rb +41 -0
  32. data/lib/paperclip/geometry_parser_factory.rb +31 -0
  33. data/lib/paperclip/glue.rb +2 -8
  34. data/lib/paperclip/has_attached_file.rb +99 -0
  35. data/lib/paperclip/helpers.rb +12 -15
  36. data/lib/paperclip/interpolations/plural_cache.rb +17 -0
  37. data/lib/paperclip/interpolations.rb +15 -5
  38. data/lib/paperclip/io_adapters/abstract_adapter.rb +45 -0
  39. data/lib/paperclip/io_adapters/attachment_adapter.rb +14 -49
  40. data/lib/paperclip/io_adapters/data_uri_adapter.rb +27 -0
  41. data/lib/paperclip/io_adapters/empty_string_adapter.rb +18 -0
  42. data/lib/paperclip/io_adapters/file_adapter.rb +8 -69
  43. data/lib/paperclip/io_adapters/identity_adapter.rb +1 -1
  44. data/lib/paperclip/io_adapters/nil_adapter.rb +2 -2
  45. data/lib/paperclip/io_adapters/stringio_adapter.rb +16 -45
  46. data/lib/paperclip/io_adapters/uploaded_file_adapter.rb +17 -40
  47. data/lib/paperclip/io_adapters/uri_adapter.rb +44 -0
  48. data/lib/paperclip/matchers/have_attached_file_matcher.rb +1 -5
  49. data/lib/paperclip/matchers/validate_attachment_content_type_matcher.rb +36 -17
  50. data/lib/paperclip/matchers/validate_attachment_presence_matcher.rb +5 -1
  51. data/lib/paperclip/matchers.rb +3 -3
  52. data/lib/paperclip/missing_attachment_styles.rb +11 -16
  53. data/lib/paperclip/processor.rb +12 -0
  54. data/lib/paperclip/railtie.rb +5 -1
  55. data/lib/paperclip/schema.rb +59 -23
  56. data/lib/paperclip/storage/filesystem.rb +23 -5
  57. data/lib/paperclip/storage/fog.rb +64 -25
  58. data/lib/paperclip/storage/s3.rb +93 -52
  59. data/lib/paperclip/style.rb +2 -2
  60. data/lib/paperclip/tempfile_factory.rb +21 -0
  61. data/lib/paperclip/thumbnail.rb +18 -3
  62. data/lib/paperclip/validators/attachment_content_type_validator.rb +38 -10
  63. data/lib/paperclip/validators/attachment_presence_validator.rb +8 -8
  64. data/lib/paperclip/validators/attachment_size_validator.rb +12 -7
  65. data/lib/paperclip/validators.rb +21 -2
  66. data/lib/paperclip/version.rb +1 -1
  67. data/lib/paperclip.rb +15 -44
  68. data/lib/tasks/paperclip.rake +26 -7
  69. data/paperclip.gemspec +11 -7
  70. data/test/attachment_definitions_test.rb +12 -0
  71. data/test/attachment_processing_test.rb +83 -0
  72. data/test/attachment_registry_test.rb +77 -0
  73. data/test/attachment_test.rb +253 -44
  74. data/test/content_type_detector_test.rb +50 -0
  75. data/test/file_command_content_type_detector_test.rb +25 -0
  76. data/test/filename_cleaner_test.rb +14 -0
  77. data/test/fixtures/animated +0 -0
  78. data/test/fixtures/animated.unknown +0 -0
  79. data/test/fixtures/rotated.jpg +0 -0
  80. data/test/generator_test.rb +26 -24
  81. data/test/geometry_detector_test.rb +24 -0
  82. data/test/geometry_parser_test.rb +73 -0
  83. data/test/geometry_test.rb +55 -4
  84. data/test/has_attached_file_test.rb +125 -0
  85. data/test/helper.rb +38 -7
  86. data/test/integration_test.rb +105 -89
  87. data/test/interpolations_test.rb +12 -0
  88. data/test/io_adapters/abstract_adapter_test.rb +58 -0
  89. data/test/io_adapters/attachment_adapter_test.rb +120 -33
  90. data/test/io_adapters/data_uri_adapter_test.rb +60 -0
  91. data/test/io_adapters/empty_string_adapter_test.rb +17 -0
  92. data/test/io_adapters/file_adapter_test.rb +32 -1
  93. data/test/io_adapters/stringio_adapter_test.rb +29 -10
  94. data/test/io_adapters/uploaded_file_adapter_test.rb +53 -5
  95. data/test/io_adapters/uri_adapter_test.rb +102 -0
  96. data/test/matchers/validate_attachment_presence_matcher_test.rb +22 -0
  97. data/test/meta_class_test.rb +32 -0
  98. data/test/paperclip_missing_attachment_styles_test.rb +4 -8
  99. data/test/paperclip_test.rb +27 -51
  100. data/test/plural_cache_test.rb +36 -0
  101. data/test/processor_test.rb +16 -0
  102. data/test/rake_test.rb +103 -0
  103. data/test/schema_test.rb +179 -77
  104. data/test/storage/filesystem_test.rb +26 -3
  105. data/test/storage/fog_test.rb +181 -3
  106. data/test/storage/s3_test.rb +239 -4
  107. data/test/style_test.rb +18 -14
  108. data/test/tempfile_factory_test.rb +13 -0
  109. data/test/thumbnail_test.rb +96 -16
  110. data/test/validators/attachment_content_type_validator_test.rb +181 -55
  111. data/test/validators/attachment_size_validator_test.rb +10 -0
  112. data/test/validators_test.rb +8 -1
  113. metadata +126 -92
  114. data/Gemfile.lock +0 -157
  115. data/features/support/fixtures/.boot_config.rb.swo +0 -0
  116. data/images.rake +0 -21
  117. data/lib/.DS_Store +0 -0
  118. data/lib/paperclip/.DS_Store +0 -0
  119. data/lib/paperclip/attachment_options.rb +0 -9
  120. data/lib/paperclip/instance_methods.rb +0 -35
  121. data/test/attachment_options_test.rb +0 -27
@@ -12,7 +12,9 @@ module Paperclip
12
12
  :convert_options => {},
13
13
  :default_style => :original,
14
14
  :default_url => "/:attachment/:style/missing.png",
15
+ :escape_url => true,
15
16
  :restricted_characters => /[&$+,\/:;=?@<>\[\]\{\}\|\\\^~%# ]/,
17
+ :filename_cleaner => nil,
16
18
  :hash_data => ":class/:attachment/:id/:style/:updated_at",
17
19
  :hash_digest => "SHA1",
18
20
  :interpolator => Paperclip::Interpolations,
@@ -27,7 +29,8 @@ module Paperclip
27
29
  :url_generator => Paperclip::UrlGenerator,
28
30
  :use_default_time_zone => true,
29
31
  :use_timestamp => true,
30
- :whiny => Paperclip.options[:whiny] || Paperclip.options[:whiny_thumbnails]
32
+ :whiny => Paperclip.options[:whiny] || Paperclip.options[:whiny_thumbnails],
33
+ :check_validity_before_processing => true
31
34
  }
32
35
  end
33
36
 
@@ -36,7 +39,7 @@ module Paperclip
36
39
  attr_accessor :post_processing
37
40
 
38
41
  # Creates an Attachment object. +name+ is the name of the attachment,
39
- # +instance+ is the ActiveRecord object instance it's attached to, and
42
+ # +instance+ is the model object instance it's attached to, and
40
43
  # +options+ is the same as the hash passed to +has_attached_file+.
41
44
  #
42
45
  # Options include:
@@ -46,7 +49,7 @@ module Paperclip
46
49
  # +styles+ - a hash of options for processing the attachment. See +has_attached_file+ for the details
47
50
  # +only_process+ - style args to be run through the post-processor. This defaults to the empty list
48
51
  # +default_url+ - a URL for the missing image
49
- # +default_style+ - the style to use when don't specify an argument to e.g. #url, #path
52
+ # +default_style+ - the style to use when an argument is not specified e.g. #url, #path
50
53
  # +storage+ - the storage mechanism. Defaults to :filesystem
51
54
  # +use_timestamp+ - whether to append an anti-caching timestamp to image URLs. Defaults to true
52
55
  # +whiny+, +whiny_thumbnails+ - whether to raise when thumbnailing fails
@@ -57,9 +60,11 @@ module Paperclip
57
60
  # +convert_options+ - flags passed to the +convert+ command for processing
58
61
  # +source_file_options+ - flags passed to the +convert+ command that controls how the file is read
59
62
  # +processors+ - classes that transform the attachment. Defaults to [:thumbnail]
60
- # +preserve_files+ - whether to keep files on the filesystem when deleting to clearing the attachment. Defaults to false
63
+ # +preserve_files+ - whether to keep files on the filesystem when deleting or clearing the attachment. Defaults to false
64
+ # +filename_cleaner+ - An object that responds to #call(filename) that will strip unacceptable charcters from filename
61
65
  # +interpolator+ - the object used to interpolate filenames and URLs. Defaults to Paperclip::Interpolations
62
66
  # +url_generator+ - the object used to generate URLs, using the interpolator. Defaults to Paperclip::UrlGenerator
67
+ # +escape_url+ - Perform URI escaping to URLs. Defaults to true
63
68
  def initialize(name, instance, options = {})
64
69
  @name = name
65
70
  @instance = instance
@@ -90,8 +95,8 @@ module Paperclip
90
95
  ensure_required_accessors!
91
96
  file = Paperclip.io_adapters.for(uploaded_file)
92
97
 
93
- @options[:only_process].map!(&:to_sym)
94
- self.clear(*@options[:only_process])
98
+ return nil if not file.assignment?
99
+ self.clear(*only_process)
95
100
  return nil if file.nil?
96
101
 
97
102
  @queued_for_write[:original] = file
@@ -99,15 +104,18 @@ module Paperclip
99
104
  instance_write(:content_type, file.content_type.to_s.strip)
100
105
  instance_write(:file_size, file.size)
101
106
  instance_write(:fingerprint, file.fingerprint) if instance_respond_to?(:fingerprint)
107
+ instance_write(:created_at, Time.now) if has_enabled_but_unset_created_at?
102
108
  instance_write(:updated_at, Time.now)
103
109
 
104
110
  @dirty = true
105
111
 
106
- post_process(*@options[:only_process]) if post_processing
112
+ post_process(*only_process) if post_processing
107
113
 
108
114
  # Reset the file size if the original file was reprocessed.
109
115
  instance_write(:file_size, @queued_for_write[:original].size)
110
116
  instance_write(:fingerprint, @queued_for_write[:original].fingerprint) if instance_respond_to?(:fingerprint)
117
+ updater = :"#{name}_file_name_will_change!"
118
+ instance.send updater if instance.respond_to? updater
111
119
  end
112
120
 
113
121
  # Returns the public URL of the attachment with a given style. This does
@@ -133,7 +141,7 @@ module Paperclip
133
141
  # +#new(Paperclip::Attachment, options_hash)+
134
142
  # +#for(style_name, options_hash)+
135
143
  def url(style_name = default_style, options = {})
136
- default_options = {:timestamp => @options[:use_timestamp], :escape => true}
144
+ default_options = {:timestamp => @options[:use_timestamp], :escape => @options[:escape_url]}
137
145
 
138
146
  if options == true || options == false # Backwards compatibility.
139
147
  @url_generator.for(style_name, default_options.merge(:timestamp => options))
@@ -142,6 +150,13 @@ module Paperclip
142
150
  end
143
151
  end
144
152
 
153
+ # Alias to +url+ that allows using the expiring_url method provided by the cloud
154
+ # storage implementations, but keep using filesystem storage for development and
155
+ # testing.
156
+ def expiring_url(time = 3600, style_name = default_style)
157
+ url(style_name)
158
+ end
159
+
145
160
  # Returns the path of the attachment as defined by the :path option. If the
146
161
  # file is stored in the filesystem the path refers to the path of the file
147
162
  # on disk. If the file is stored in S3, the path is the "key" part of the
@@ -156,6 +171,10 @@ module Paperclip
156
171
  url(style_name)
157
172
  end
158
173
 
174
+ def as_json(options = nil)
175
+ to_s((options && options[:style]) || default_style)
176
+ end
177
+
159
178
  def default_style
160
179
  @options[:default_style]
161
180
  end
@@ -166,13 +185,19 @@ module Paperclip
166
185
  styles = styles.call(self) if styles.respond_to?(:call)
167
186
 
168
187
  @normalized_styles = styles.dup
169
- @normalized_styles.each_pair do |name, options|
188
+ styles.each_pair do |name, options|
170
189
  @normalized_styles[name.to_sym] = Paperclip::Style.new(name.to_sym, options.dup, self)
171
190
  end
172
191
  end
173
192
  @normalized_styles
174
193
  end
175
194
 
195
+ def only_process
196
+ only_process = @options[:only_process].dup
197
+ only_process = only_process.call(self) if only_process.respond_to?(:call)
198
+ only_process.map(&:to_sym)
199
+ end
200
+
176
201
  def processors
177
202
  processing_option = @options[:processors]
178
203
 
@@ -219,10 +244,8 @@ module Paperclip
219
244
  # nil to the attachment *and saving*. This is permanent. If you wish to
220
245
  # wipe out the existing attachment but not save, use #clear.
221
246
  def destroy
222
- unless @options[:preserve_files]
223
- clear
224
- save
225
- end
247
+ clear
248
+ save
226
249
  end
227
250
 
228
251
  # Returns the uploaded file if present.
@@ -243,7 +266,7 @@ module Paperclip
243
266
  end
244
267
 
245
268
  # Returns the fingerprint of the file, if one's defined. The fingerprint is
246
- # stored in the <attachment>_fingerpring attribute of the model.
269
+ # stored in the <attachment>_fingerprint attribute of the model.
247
270
  def fingerprint
248
271
  instance_read(:fingerprint)
249
272
  end
@@ -254,6 +277,15 @@ module Paperclip
254
277
  instance_read(:content_type)
255
278
  end
256
279
 
280
+ # Returns the creation time of the file as originally assigned, and
281
+ # lives in the <attachment>_created_at attribute of the model.
282
+ def created_at
283
+ if able_to_store_created_at?
284
+ time = instance_read(:created_at)
285
+ time && time.to_f.to_i
286
+ end
287
+ end
288
+
257
289
  # Returns the last modified time of the file as originally assigned, and
258
290
  # lives in the <attachment>_updated_at attribute of the model.
259
291
  def updated_at
@@ -285,6 +317,7 @@ module Paperclip
285
317
  begin
286
318
  assign(self)
287
319
  save
320
+ instance.save
288
321
  rescue Errno::EACCES => e
289
322
  warn "#{e} - skipping file."
290
323
  false
@@ -300,6 +333,10 @@ module Paperclip
300
333
 
301
334
  alias :present? :file?
302
335
 
336
+ def blank?
337
+ not present?
338
+ end
339
+
303
340
  # Determines whether the instance responds to this attribute. Used to prevent
304
341
  # calculations on fields we won't even store.
305
342
  def instance_respond_to?(attr)
@@ -311,19 +348,18 @@ module Paperclip
311
348
  # "avatar_file_name" field (assuming the attachment is called avatar).
312
349
  def instance_write(attr, value)
313
350
  setter = :"#{name}_#{attr}="
314
- responds = instance.respond_to?(setter)
315
- self.instance_variable_set("@_#{setter.to_s.chop}", value)
316
- instance.send(setter, value) if responds || attr.to_s == "file_name"
351
+ if instance.respond_to?(setter)
352
+ instance.send(setter, value)
353
+ end
317
354
  end
318
355
 
319
356
  # Reads the attachment-specific attribute on the instance. See instance_write
320
357
  # for more details.
321
358
  def instance_read(attr)
322
359
  getter = :"#{name}_#{attr}"
323
- responds = instance.respond_to?(getter)
324
- cached = self.instance_variable_get("@_#{getter}")
325
- return cached if cached
326
- instance.send(getter) if responds || attr.to_s == "file_name"
360
+ if instance.respond_to?(getter)
361
+ instance.send(getter)
362
+ end
327
363
  end
328
364
 
329
365
  private
@@ -344,10 +380,6 @@ module Paperclip
344
380
  Paperclip.log(message)
345
381
  end
346
382
 
347
- def valid_assignment? file #:nodoc:
348
- file.nil? || (file.respond_to?(:original_filename) && file.respond_to?(:content_type))
349
- end
350
-
351
383
  def initialize_storage #:nodoc:
352
384
  storage_class_name = @options[:storage].to_s.downcase.camelize
353
385
  begin
@@ -359,18 +391,17 @@ module Paperclip
359
391
  end
360
392
 
361
393
  def extra_options_for(style) #:nodoc:
362
- all_options = @options[:convert_options][:all]
363
- all_options = all_options.call(instance) if all_options.respond_to?(:call)
364
- style_options = @options[:convert_options][style]
365
- style_options = style_options.call(instance) if style_options.respond_to?(:call)
366
-
367
- [ style_options, all_options ].compact.join(" ")
394
+ process_options(:convert_options, style)
368
395
  end
369
396
 
370
397
  def extra_source_file_options_for(style) #:nodoc:
371
- all_options = @options[:source_file_options][:all]
398
+ process_options(:source_file_options, style)
399
+ end
400
+
401
+ def process_options(options_type, style) #:nodoc:
402
+ all_options = @options[options_type][:all]
372
403
  all_options = all_options.call(instance) if all_options.respond_to?(:call)
373
- style_options = @options[:source_file_options][style]
404
+ style_options = @options[options_type][style]
374
405
  style_options = style_options.call(instance) if style_options.respond_to?(:call)
375
406
 
376
407
  [ style_options, all_options ].compact.join(" ")
@@ -381,7 +412,9 @@ module Paperclip
381
412
 
382
413
  instance.run_paperclip_callbacks(:post_process) do
383
414
  instance.run_paperclip_callbacks(:"#{name}_post_process") do
384
- post_process_styles(*style_args)
415
+ unless @options[:check_validity_before_processing] && instance.errors.any?
416
+ post_process_styles(*style_args)
417
+ end
385
418
  end
386
419
  end
387
420
  end
@@ -421,14 +454,17 @@ module Paperclip
421
454
  end
422
455
 
423
456
  def queue_all_for_delete #:nodoc:
424
- return if @options[:preserve_files] || !file?
425
- @queued_for_delete += [:original, *styles.keys].uniq.map do |style|
426
- path(style) if exists?(style)
427
- end.compact
457
+ return if !file?
458
+ unless @options[:preserve_files]
459
+ @queued_for_delete += [:original, *styles.keys].uniq.map do |style|
460
+ path(style) if exists?(style)
461
+ end.compact
462
+ end
428
463
  instance_write(:file_name, nil)
429
464
  instance_write(:content_type, nil)
430
465
  instance_write(:file_size, nil)
431
466
  instance_write(:fingerprint, nil)
467
+ instance_write(:created_at, nil) if has_enabled_but_unset_created_at?
432
468
  instance_write(:updated_at, nil)
433
469
  end
434
470
 
@@ -440,14 +476,31 @@ module Paperclip
440
476
 
441
477
  # called by storage after the writes are flushed and before @queued_for_writes is cleared
442
478
  def after_flush_writes
479
+ @queued_for_write.each do |style, file|
480
+ file.close unless file.closed?
481
+ file.unlink if file.respond_to?(:unlink) && file.path.present? && File.exist?(file.path)
482
+ end
483
+ end
484
+
485
+ # You can either specifiy :restricted_characters or you can define your own
486
+ # :filename_cleaner object. This object needs to respond to #call and takes
487
+ # the filename that will be cleaned. It should return the cleaned filenme.
488
+ def filename_cleaner
489
+ @options[:filename_cleaner] || FilenameCleaner.new(@options[:restricted_characters])
443
490
  end
444
491
 
445
492
  def cleanup_filename(filename)
446
- if @options[:restricted_characters]
447
- filename.gsub(@options[:restricted_characters], '_')
448
- else
449
- filename
450
- end
493
+ filename_cleaner.call(filename)
494
+ end
495
+
496
+ # Check if attachment database table has a created_at field
497
+ def able_to_store_created_at?
498
+ @instance.respond_to?("#{name}_created_at".to_sym)
499
+ end
500
+
501
+ # Check if attachment database table has a created_at field which is not yet set
502
+ def has_enabled_but_unset_created_at?
503
+ able_to_store_created_at? && !instance_read(:created_at)
451
504
  end
452
505
  end
453
506
  end
@@ -0,0 +1,57 @@
1
+ require 'singleton'
2
+
3
+ module Paperclip
4
+ class AttachmentRegistry
5
+ include Singleton
6
+
7
+ def self.register(klass, attachment_name, attachment_options)
8
+ instance.register(klass, attachment_name, attachment_options)
9
+ end
10
+
11
+ def self.clear
12
+ instance.clear
13
+ end
14
+
15
+ def self.names_for(klass)
16
+ instance.names_for(klass)
17
+ end
18
+
19
+ def self.each_definition(&block)
20
+ instance.each_definition(&block)
21
+ end
22
+
23
+ def self.definitions_for(klass)
24
+ instance.definitions_for(klass)
25
+ end
26
+
27
+ def initialize
28
+ clear
29
+ end
30
+
31
+ def register(klass, attachment_name, attachment_options)
32
+ @attachments ||= {}
33
+ @attachments[klass] ||= {}
34
+ @attachments[klass][attachment_name] = attachment_options
35
+ end
36
+
37
+ def clear
38
+ @attachments = Hash.new { |h,k| h[k] = {} }
39
+ end
40
+
41
+ def names_for(klass)
42
+ @attachments[klass].keys
43
+ end
44
+
45
+ def each_definition
46
+ @attachments.each do |klass, attachments|
47
+ attachments.each do |name, options|
48
+ yield klass, name, options
49
+ end
50
+ end
51
+ end
52
+
53
+ def definitions_for(klass)
54
+ @attachments[klass]
55
+ end
56
+ end
57
+ end
@@ -22,8 +22,8 @@ module Paperclip
22
22
  end
23
23
 
24
24
  module Running
25
- def run_paperclip_callbacks(callback, opts = nil, &block)
26
- run_callbacks(callback, opts, &block)
25
+ def run_paperclip_callbacks(callback, &block)
26
+ run_callbacks(callback, &block)
27
27
  end
28
28
  end
29
29
  end
@@ -0,0 +1,78 @@
1
+ module Paperclip
2
+ class ContentTypeDetector
3
+ # The content-type detection strategy is as follows:
4
+ #
5
+ # 1. Blank/Empty files: If there's no filename or the file is empty,
6
+ # provide a sensible default (application/octet-stream or inode/x-empty)
7
+ #
8
+ # 2. Calculated match: Return the first result that is found by both the
9
+ # `file` command and MIME::Types.
10
+ #
11
+ # 3. Standard types: Return the first standard (without an x- prefix) entry
12
+ # in MIME::Types
13
+ #
14
+ # 4. Experimental types: If there were no standard types in MIME::Types
15
+ # list, try to return the first experimental one
16
+ #
17
+ # 5. Raw `file` command: Just use the output of the `file` command raw, or
18
+ # a sensible default. This is cached from Step 2.
19
+
20
+ EMPTY_TYPE = "inode/x-empty"
21
+ SENSIBLE_DEFAULT = "application/octet-stream"
22
+
23
+ def initialize(filename)
24
+ @filename = filename
25
+ end
26
+
27
+ # Returns a String describing the file's content type
28
+ def detect
29
+ if blank_name?
30
+ SENSIBLE_DEFAULT
31
+ elsif empty_file?
32
+ EMPTY_TYPE
33
+ elsif calculated_type_matches.any?
34
+ calculated_type_matches.first
35
+ elsif official_type_matches.any?
36
+ official_type_matches.first
37
+ elsif unofficial_type_matches.any?
38
+ unofficial_type_matches.first
39
+ else
40
+ type_from_file_command || SENSIBLE_DEFAULT
41
+ end.to_s
42
+ end
43
+
44
+ private
45
+
46
+ def empty_file?
47
+ File.exists?(@filename) && File.size(@filename) == 0
48
+ end
49
+
50
+ def blank_name?
51
+ @filename.nil? || @filename.empty?
52
+ end
53
+
54
+ def empty?
55
+ File.exists?(@filename) && File.size(@filename) == 0
56
+ end
57
+
58
+ def possible_types
59
+ MIME::Types.type_for(@filename).collect(&:content_type)
60
+ end
61
+
62
+ def calculated_type_matches
63
+ possible_types.select{|content_type| content_type == type_from_file_command }
64
+ end
65
+
66
+ def official_type_matches
67
+ possible_types.reject{|content_type| content_type.match(/\/x-/) }
68
+ end
69
+
70
+ def unofficial_type_matches
71
+ possible_types.select{|content_type| content_type.match(/\/x-/) }
72
+ end
73
+
74
+ def type_from_file_command
75
+ @type_from_file_command ||= FileCommandContentTypeDetector.new(@filename).detect
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,32 @@
1
+ module Paperclip
2
+ class FileCommandContentTypeDetector
3
+ SENSIBLE_DEFAULT = "application/octet-stream"
4
+
5
+ def initialize(filename)
6
+ @filename = filename
7
+ end
8
+
9
+ def detect
10
+ type_from_file_command
11
+ end
12
+
13
+ private
14
+
15
+ def type_from_file_command
16
+ type = begin
17
+ # On BSDs, `file` doesn't give a result code of 1 if the file doesn't exist.
18
+ Paperclip.run("file", "-b --mime :file", :file => @filename)
19
+ rescue Cocaine::CommandLineError => e
20
+ Paperclip.log("Error while determining content type: #{e}")
21
+ SENSIBLE_DEFAULT
22
+ end
23
+
24
+ if type.nil? || type.match(/\(.*?\)/)
25
+ type = SENSIBLE_DEFAULT
26
+ end
27
+ type.split(/[:;\s]+/)[0]
28
+ end
29
+
30
+ end
31
+ end
32
+
@@ -0,0 +1,16 @@
1
+ # encoding: utf-8
2
+ module Paperclip
3
+ class FilenameCleaner
4
+ def initialize(invalid_character_regex)
5
+ @invalid_character_regex = invalid_character_regex
6
+ end
7
+
8
+ def call(filename)
9
+ if @invalid_character_regex
10
+ filename.gsub(@invalid_character_regex, "_")
11
+ else
12
+ filename
13
+ end
14
+ end
15
+ end
16
+ end
@@ -4,37 +4,40 @@ module Paperclip
4
4
  class Geometry
5
5
  attr_accessor :height, :width, :modifier
6
6
 
7
+ EXIF_ROTATED_ORIENTATION_VALUES = [5, 6, 7, 8]
8
+
7
9
  # Gives a Geometry representing the given height and width
8
- def initialize width = nil, height = nil, modifier = nil
9
- @height = height.to_f
10
- @width = width.to_f
11
- @modifier = modifier
12
- end
13
-
14
- # Uses ImageMagick to determing the dimensions of a file, passed in as either a
15
- # File or path.
16
- # NOTE: (race cond) Do not reassign the 'file' variable inside this method as it is likely to be
17
- # a Tempfile object, which would be eligible for file deletion when no longer referenced.
18
- def self.from_file file
19
- file_path = file.respond_to?(:path) ? file.path : file
20
- raise(Errors::NotIdentifiedByImageMagickError.new("Cannot find the geometry of a file with a blank name")) if file_path.blank?
21
- geometry = begin
22
- silence_stream(STDERR) do
23
- Paperclip.run("identify", "-format %wx%h :file", :file => "#{file_path}[0]")
24
- end
25
- rescue Cocaine::ExitStatusError
26
- ""
27
- rescue Cocaine::CommandNotFoundError => e
28
- raise Errors::CommandNotFoundError.new("Could not run the `identify` command. Please install ImageMagick.")
29
- end
30
- parse(geometry) ||
31
- raise(Errors::NotIdentifiedByImageMagickError.new("#{file_path} is not recognized by the 'identify' command."))
32
- end
33
-
34
- # Parses a "WxH" formatted string, where W is the width and H is the height.
35
- def self.parse string
36
- if match = (string && string.match(/\b(\d*)x?(\d*)\b([\>\<\#\@\%^!])?/i))
37
- Geometry.new(*match[1,3])
10
+ def initialize(width = nil, height = nil, modifier = nil)
11
+ if width.is_a?(Hash)
12
+ options = width
13
+ @height = options[:height].to_f
14
+ @width = options[:width].to_f
15
+ @modifier = options[:modifier]
16
+ @orientation = options[:orientation].to_i
17
+ else
18
+ @height = height.to_f
19
+ @width = width.to_f
20
+ @modifier = modifier
21
+ end
22
+ end
23
+
24
+ # Extracts the Geometry from a file (or path to a file)
25
+ def self.from_file(file)
26
+ GeometryDetector.new(file).make
27
+ end
28
+
29
+ # Extracts the Geometry from a "WxH,O" string
30
+ # Where W is the width, H is the height,
31
+ # and O is the EXIF orientation
32
+ def self.parse(string)
33
+ GeometryParser.new(string).make
34
+ end
35
+
36
+ # Swaps the height and width if necessary
37
+ def auto_orient
38
+ if EXIF_ROTATED_ORIENTATION_VALUES.include?(@orientation)
39
+ @height, @width = @width, @height
40
+ @orientation -= 4
38
41
  end
39
42
  end
40
43
 
@@ -101,6 +104,33 @@ module Paperclip
101
104
  [ scale_geometry, crop_geometry ]
102
105
  end
103
106
 
107
+ # resize to a new geometry
108
+ # @param geometry [String] the Paperclip geometry definition to resize to
109
+ # @example
110
+ # Paperclip::Geometry.new(150, 150).resize_to('50x50!')
111
+ # #=> Paperclip::Geometry(50, 50)
112
+ def resize_to(geometry)
113
+ new_geometry = Paperclip::Geometry.parse geometry
114
+ case new_geometry.modifier
115
+ when '!', '#'
116
+ new_geometry
117
+ when '>'
118
+ if new_geometry.width >= self.width && new_geometry.height >= self.height
119
+ self
120
+ else
121
+ scale_to new_geometry
122
+ end
123
+ when '<'
124
+ if new_geometry.width <= self.width || new_geometry.height <= self.height
125
+ self
126
+ else
127
+ scale_to new_geometry
128
+ end
129
+ else
130
+ scale_to new_geometry
131
+ end
132
+ end
133
+
104
134
  private
105
135
 
106
136
  def scaling dst, ratio
@@ -118,5 +148,11 @@ module Paperclip
118
148
  "%dx%d+%d+%d" % [ dst.width, dst.height, (self.width * scale - dst.width) / 2, 0 ]
119
149
  end
120
150
  end
151
+
152
+ # scale to the requested geometry and preserve the aspect ratio
153
+ def scale_to(new_geometry)
154
+ scale = [new_geometry.width.to_f / self.width.to_f , new_geometry.height.to_f / self.height.to_f].min
155
+ Paperclip::Geometry.new((self.width * scale).round, (self.height * scale).round)
156
+ end
121
157
  end
122
158
  end
@@ -0,0 +1,41 @@
1
+ module Paperclip
2
+ class GeometryDetector
3
+ def initialize(file)
4
+ @file = file
5
+ raise_if_blank_file
6
+ end
7
+
8
+ def make
9
+ geometry = GeometryParser.new(geometry_string.strip).make
10
+ geometry || raise(Errors::NotIdentifiedByImageMagickError.new)
11
+ end
12
+
13
+ private
14
+
15
+ def geometry_string
16
+ begin
17
+ silence_stream(STDERR) do
18
+ Paperclip.run("identify", "-format '%wx%h,%[exif:orientation]' :file", :file => "#{path}[0]")
19
+ end
20
+ rescue Cocaine::ExitStatusError
21
+ ""
22
+ rescue Cocaine::CommandNotFoundError => e
23
+ raise_because_imagemagick_missing
24
+ end
25
+ end
26
+
27
+ def path
28
+ @file.respond_to?(:path) ? @file.path : @file
29
+ end
30
+
31
+ def raise_if_blank_file
32
+ if path.blank?
33
+ raise Errors::NotIdentifiedByImageMagickError.new("Cannot find the geometry of a file with a blank name")
34
+ end
35
+ end
36
+
37
+ def raise_because_imagemagick_missing
38
+ raise Errors::CommandNotFoundError.new("Could not run the `identify` command. Please install ImageMagick.")
39
+ end
40
+ end
41
+ end