activestorage 5.2.4.4 → 6.1.1

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

Potentially problematic release.


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

Files changed (77) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +180 -69
  3. data/MIT-LICENSE +1 -1
  4. data/README.md +43 -8
  5. data/app/assets/javascripts/activestorage.js +5 -2
  6. data/app/controllers/active_storage/base_controller.rb +13 -4
  7. data/app/controllers/active_storage/blobs/proxy_controller.rb +14 -0
  8. data/app/controllers/active_storage/{blobs_controller.rb → blobs/redirect_controller.rb} +3 -3
  9. data/app/controllers/active_storage/direct_uploads_controller.rb +2 -2
  10. data/app/controllers/active_storage/disk_controller.rb +13 -22
  11. data/app/controllers/active_storage/representations/proxy_controller.rb +19 -0
  12. data/app/controllers/active_storage/{representations_controller.rb → representations/redirect_controller.rb} +3 -3
  13. data/app/controllers/concerns/active_storage/file_server.rb +18 -0
  14. data/app/controllers/concerns/active_storage/set_blob.rb +1 -1
  15. data/app/controllers/concerns/active_storage/set_current.rb +15 -0
  16. data/app/controllers/concerns/active_storage/set_headers.rb +12 -0
  17. data/app/javascript/activestorage/blob_record.js +7 -2
  18. data/app/jobs/active_storage/analyze_job.rb +5 -0
  19. data/app/jobs/active_storage/base_job.rb +0 -1
  20. data/app/jobs/active_storage/mirror_job.rb +15 -0
  21. data/app/jobs/active_storage/purge_job.rb +3 -0
  22. data/app/models/active_storage/attachment.rb +35 -16
  23. data/app/models/active_storage/blob.rb +178 -68
  24. data/app/models/active_storage/blob/analyzable.rb +6 -2
  25. data/app/models/active_storage/blob/identifiable.rb +7 -6
  26. data/app/models/active_storage/blob/representable.rb +36 -6
  27. data/app/models/active_storage/filename.rb +0 -6
  28. data/app/models/active_storage/preview.rb +37 -12
  29. data/app/models/active_storage/record.rb +7 -0
  30. data/app/models/active_storage/variant.rb +53 -67
  31. data/app/models/active_storage/variant_record.rb +8 -0
  32. data/app/models/active_storage/variant_with_record.rb +54 -0
  33. data/app/models/active_storage/variation.rb +30 -34
  34. data/config/routes.rb +66 -15
  35. data/db/migrate/20170806125915_create_active_storage_tables.rb +14 -5
  36. data/db/update_migrate/20190112182829_add_service_name_to_active_storage_blobs.rb +17 -0
  37. data/db/update_migrate/20191206030411_create_active_storage_variant_records.rb +11 -0
  38. data/lib/active_storage.rb +29 -6
  39. data/lib/active_storage/analyzer.rb +15 -4
  40. data/lib/active_storage/analyzer/image_analyzer.rb +14 -4
  41. data/lib/active_storage/analyzer/null_analyzer.rb +4 -0
  42. data/lib/active_storage/analyzer/video_analyzer.rb +17 -8
  43. data/lib/active_storage/attached.rb +7 -22
  44. data/lib/active_storage/attached/changes.rb +16 -0
  45. data/lib/active_storage/attached/changes/create_many.rb +47 -0
  46. data/lib/active_storage/attached/changes/create_one.rb +82 -0
  47. data/lib/active_storage/attached/changes/create_one_of_many.rb +10 -0
  48. data/lib/active_storage/attached/changes/delete_many.rb +27 -0
  49. data/lib/active_storage/attached/changes/delete_one.rb +19 -0
  50. data/lib/active_storage/attached/many.rb +19 -12
  51. data/lib/active_storage/attached/model.rb +212 -0
  52. data/lib/active_storage/attached/one.rb +19 -21
  53. data/lib/active_storage/downloader.rb +43 -0
  54. data/lib/active_storage/engine.rb +58 -23
  55. data/lib/active_storage/errors.rb +22 -3
  56. data/lib/active_storage/gem_version.rb +4 -4
  57. data/lib/active_storage/log_subscriber.rb +6 -0
  58. data/lib/active_storage/previewer.rb +24 -13
  59. data/lib/active_storage/previewer/mupdf_previewer.rb +3 -3
  60. data/lib/active_storage/previewer/poppler_pdf_previewer.rb +5 -5
  61. data/lib/active_storage/previewer/video_previewer.rb +17 -10
  62. data/lib/active_storage/reflection.rb +64 -0
  63. data/lib/active_storage/service.rb +44 -12
  64. data/lib/active_storage/service/azure_storage_service.rb +65 -44
  65. data/lib/active_storage/service/configurator.rb +6 -2
  66. data/lib/active_storage/service/disk_service.rb +57 -44
  67. data/lib/active_storage/service/gcs_service.rb +68 -64
  68. data/lib/active_storage/service/mirror_service.rb +31 -7
  69. data/lib/active_storage/service/registry.rb +32 -0
  70. data/lib/active_storage/service/s3_service.rb +58 -24
  71. data/lib/active_storage/transformers/image_processing_transformer.rb +45 -0
  72. data/lib/active_storage/transformers/transformer.rb +39 -0
  73. data/lib/tasks/activestorage.rake +7 -0
  74. metadata +84 -19
  75. data/app/models/active_storage/filename/parameters.rb +0 -36
  76. data/lib/active_storage/attached/macros.rb +0 -110
  77. data/lib/active_storage/downloading.rb +0 -39
@@ -3,39 +3,42 @@
3
3
  module ActiveStorage
4
4
  # Representation of a single attachment to a model.
5
5
  class Attached::One < Attached
6
- delegate_missing_to :attachment
6
+ delegate_missing_to :attachment, allow_nil: true
7
7
 
8
8
  # Returns the associated attachment record.
9
9
  #
10
10
  # You don't have to call this method to access the attachment's methods as
11
11
  # they are all available at the model level.
12
12
  def attachment
13
- record.public_send("#{name}_attachment")
13
+ change.present? ? change.attachment : record.public_send("#{name}_attachment")
14
14
  end
15
15
 
16
- # Associates a given attachment with the current record, saving it to the database.
16
+ def blank?
17
+ !attached?
18
+ end
19
+
20
+ # Attaches an +attachable+ to the record.
21
+ #
22
+ # If the record is persisted and unchanged, the attachment is saved to
23
+ # the database immediately. Otherwise, it'll be saved to the DB when the
24
+ # record is next saved.
17
25
  #
18
26
  # person.avatar.attach(params[:avatar]) # ActionDispatch::Http::UploadedFile object
19
27
  # person.avatar.attach(params[:signed_blob_id]) # Signed reference to blob from direct upload
20
28
  # person.avatar.attach(io: File.open("/path/to/face.jpg"), filename: "face.jpg", content_type: "image/jpg")
21
29
  # person.avatar.attach(avatar_blob) # ActiveStorage::Blob object
22
30
  def attach(attachable)
23
- blob_was = blob if attached?
24
- blob = create_blob_from(attachable)
25
-
26
- unless blob == blob_was
27
- transaction do
28
- detach
29
- write_attachment build_attachment(blob: blob)
30
- end
31
-
32
- blob_was.purge_later if blob_was && dependent == :purge_later
31
+ if record.persisted? && !record.changed?
32
+ record.public_send("#{name}=", attachable)
33
+ record.save
34
+ else
35
+ record.public_send("#{name}=", attachable)
33
36
  end
34
37
  end
35
38
 
36
39
  # Returns +true+ if an attachment has been made.
37
40
  #
38
- # class User < ActiveRecord::Base
41
+ # class User < ApplicationRecord
39
42
  # has_one_attached :avatar
40
43
  # end
41
44
  #
@@ -47,7 +50,7 @@ module ActiveStorage
47
50
  # Deletes the attachment without purging it, leaving its blob in place.
48
51
  def detach
49
52
  if attached?
50
- attachment.destroy
53
+ attachment.delete
51
54
  write_attachment nil
52
55
  end
53
56
  end
@@ -65,16 +68,11 @@ module ActiveStorage
65
68
  def purge_later
66
69
  if attached?
67
70
  attachment.purge_later
71
+ write_attachment nil
68
72
  end
69
73
  end
70
74
 
71
75
  private
72
- delegate :transaction, to: :record
73
-
74
- def build_attachment(blob:)
75
- ActiveStorage::Attachment.new(record: record, name: name, blob: blob)
76
- end
77
-
78
76
  def write_attachment(attachment)
79
77
  record.public_send("#{name}_attachment=", attachment)
80
78
  end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveStorage
4
+ class Downloader #:nodoc:
5
+ attr_reader :service
6
+
7
+ def initialize(service)
8
+ @service = service
9
+ end
10
+
11
+ def open(key, checksum:, name: "ActiveStorage-", tmpdir: nil)
12
+ open_tempfile(name, tmpdir) do |file|
13
+ download key, file
14
+ verify_integrity_of file, checksum: checksum
15
+ yield file
16
+ end
17
+ end
18
+
19
+ private
20
+ def open_tempfile(name, tmpdir = nil)
21
+ file = Tempfile.open(name, tmpdir)
22
+
23
+ begin
24
+ yield file
25
+ ensure
26
+ file.close!
27
+ end
28
+ end
29
+
30
+ def download(key, file)
31
+ file.binmode
32
+ service.download(key) { |chunk| file.write(chunk) }
33
+ file.flush
34
+ file.rewind
35
+ end
36
+
37
+ def verify_integrity_of(file, checksum:)
38
+ unless Digest::MD5.file(file).base64digest == checksum
39
+ raise ActiveStorage::IntegrityError
40
+ end
41
+ end
42
+ end
43
+ end
@@ -1,6 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "rails"
4
+ require "action_controller/railtie"
5
+ require "active_job/railtie"
6
+ require "active_record/railtie"
7
+
4
8
  require "active_storage"
5
9
 
6
10
  require "active_storage/previewer/poppler_pdf_previewer"
@@ -10,6 +14,10 @@ require "active_storage/previewer/video_previewer"
10
14
  require "active_storage/analyzer/image_analyzer"
11
15
  require "active_storage/analyzer/video_analyzer"
12
16
 
17
+ require "active_storage/service/registry"
18
+
19
+ require "active_storage/reflection"
20
+
13
21
  module ActiveStorage
14
22
  class Engine < Rails::Engine # :nodoc:
15
23
  isolate_namespace ActiveStorage
@@ -18,14 +26,26 @@ module ActiveStorage
18
26
  config.active_storage.previewers = [ ActiveStorage::Previewer::PopplerPDFPreviewer, ActiveStorage::Previewer::MuPDFPreviewer, ActiveStorage::Previewer::VideoPreviewer ]
19
27
  config.active_storage.analyzers = [ ActiveStorage::Analyzer::ImageAnalyzer, ActiveStorage::Analyzer::VideoAnalyzer ]
20
28
  config.active_storage.paths = ActiveSupport::OrderedOptions.new
29
+ config.active_storage.queues = ActiveSupport::InheritableOptions.new
21
30
 
22
31
  config.active_storage.variable_content_types = %w(
23
32
  image/png
24
33
  image/gif
25
34
  image/jpg
26
35
  image/jpeg
36
+ image/pjpeg
37
+ image/tiff
38
+ image/bmp
27
39
  image/vnd.adobe.photoshop
28
40
  image/vnd.microsoft.icon
41
+ image/webp
42
+ )
43
+
44
+ config.active_storage.web_image_content_types = %w(
45
+ image/png
46
+ image/jpeg
47
+ image/jpg
48
+ image/gif
29
49
  )
30
50
 
31
51
  config.active_storage.content_types_to_serve_as_binary = %w(
@@ -46,6 +66,8 @@ module ActiveStorage
46
66
  image/gif
47
67
  image/jpg
48
68
  image/jpeg
69
+ image/tiff
70
+ image/bmp
49
71
  image/vnd.adobe.photoshop
50
72
  image/vnd.microsoft.icon
51
73
  application/pdf
@@ -55,16 +77,24 @@ module ActiveStorage
55
77
 
56
78
  initializer "active_storage.configs" do
57
79
  config.after_initialize do |app|
58
- ActiveStorage.logger = app.config.active_storage.logger || Rails.logger
59
- ActiveStorage.queue = app.config.active_storage.queue
60
- ActiveStorage.previewers = app.config.active_storage.previewers || []
61
- ActiveStorage.analyzers = app.config.active_storage.analyzers || []
62
- ActiveStorage.paths = app.config.active_storage.paths || {}
80
+ ActiveStorage.logger = app.config.active_storage.logger || Rails.logger
81
+ ActiveStorage.variant_processor = app.config.active_storage.variant_processor || :mini_magick
82
+ ActiveStorage.previewers = app.config.active_storage.previewers || []
83
+ ActiveStorage.analyzers = app.config.active_storage.analyzers || []
84
+ ActiveStorage.paths = app.config.active_storage.paths || {}
85
+ ActiveStorage.routes_prefix = app.config.active_storage.routes_prefix || "/rails/active_storage"
86
+ ActiveStorage.draw_routes = app.config.active_storage.draw_routes != false
87
+ ActiveStorage.resolve_model_to_route = app.config.active_storage.resolve_model_to_route || :rails_storage_redirect
63
88
 
64
89
  ActiveStorage.variable_content_types = app.config.active_storage.variable_content_types || []
90
+ ActiveStorage.web_image_content_types = app.config.active_storage.web_image_content_types || []
65
91
  ActiveStorage.content_types_to_serve_as_binary = app.config.active_storage.content_types_to_serve_as_binary || []
92
+ ActiveStorage.service_urls_expire_in = app.config.active_storage.service_urls_expire_in || 5.minutes
66
93
  ActiveStorage.content_types_allowed_inline = app.config.active_storage.content_types_allowed_inline || []
67
94
  ActiveStorage.binary_content_type = app.config.active_storage.binary_content_type || "application/octet-stream"
95
+
96
+ ActiveStorage.replace_on_assign_to_many = app.config.active_storage.replace_on_assign_to_many || false
97
+ ActiveStorage.track_variants = app.config.active_storage.track_variants || false
68
98
  end
69
99
  end
70
100
 
@@ -72,7 +102,7 @@ module ActiveStorage
72
102
  require "active_storage/attached"
73
103
 
74
104
  ActiveSupport.on_load(:active_record) do
75
- extend ActiveStorage::Attached::Macros
105
+ include ActiveStorage::Attached::Model
76
106
  end
77
107
  end
78
108
 
@@ -84,29 +114,34 @@ module ActiveStorage
84
114
 
85
115
  initializer "active_storage.services" do
86
116
  ActiveSupport.on_load(:active_storage_blob) do
87
- if config_choice = Rails.configuration.active_storage.service
88
- configs = Rails.configuration.active_storage.service_configurations ||= begin
89
- config_file = Pathname.new(Rails.root.join("config/storage.yml"))
117
+ configs = Rails.configuration.active_storage.service_configurations ||=
118
+ begin
119
+ config_file = Rails.root.join("config/storage/#{Rails.env}.yml")
120
+ config_file = Rails.root.join("config/storage.yml") unless config_file.exist?
90
121
  raise("Couldn't find Active Storage configuration in #{config_file}") unless config_file.exist?
91
122
 
92
- require "yaml"
93
- require "erb"
94
-
95
- YAML.load(ERB.new(config_file.read).result) || {}
96
- rescue Psych::SyntaxError => e
97
- raise "YAML syntax error occurred while parsing #{config_file}. " \
98
- "Please note that YAML must be consistently indented using spaces. Tabs are not allowed. " \
99
- "Error: #{e.message}"
123
+ ActiveSupport::ConfigurationFile.parse(config_file)
100
124
  end
101
125
 
102
- ActiveStorage::Blob.service =
103
- begin
104
- ActiveStorage::Service.configure config_choice, configs
105
- rescue => e
106
- raise e, "Cannot load `Rails.config.active_storage.service`:\n#{e.message}", e.backtrace
107
- end
126
+ ActiveStorage::Blob.services = ActiveStorage::Service::Registry.new(configs)
127
+
128
+ if config_choice = Rails.configuration.active_storage.service
129
+ ActiveStorage::Blob.service = ActiveStorage::Blob.services.fetch(config_choice)
108
130
  end
109
131
  end
110
132
  end
133
+
134
+ initializer "active_storage.queues" do
135
+ config.after_initialize do |app|
136
+ ActiveStorage.queues = app.config.active_storage.queues || {}
137
+ end
138
+ end
139
+
140
+ initializer "active_storage.reflection" do
141
+ ActiveSupport.on_load(:active_record) do
142
+ include Reflection::ActiveRecordExtensions
143
+ ActiveRecord::Reflection.singleton_class.prepend(Reflection::ReflectionExtension)
144
+ end
145
+ end
111
146
  end
112
147
  end
@@ -1,7 +1,26 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveStorage
4
- class InvariableError < StandardError; end
5
- class UnpreviewableError < StandardError; end
6
- class UnrepresentableError < StandardError; end
4
+ # Generic base class for all Active Storage exceptions.
5
+ class Error < StandardError; end
6
+
7
+ # Raised when ActiveStorage::Blob#variant is called on a blob that isn't variable.
8
+ # Use ActiveStorage::Blob#variable? to determine whether a blob is variable.
9
+ class InvariableError < Error; end
10
+
11
+ # Raised when ActiveStorage::Blob#preview is called on a blob that isn't previewable.
12
+ # Use ActiveStorage::Blob#previewable? to determine whether a blob is previewable.
13
+ class UnpreviewableError < Error; end
14
+
15
+ # Raised when ActiveStorage::Blob#representation is called on a blob that isn't representable.
16
+ # Use ActiveStorage::Blob#representable? to determine whether a blob is representable.
17
+ class UnrepresentableError < Error; end
18
+
19
+ # Raised when uploaded or downloaded data does not match a precomputed checksum.
20
+ # Indicates that a network error or a software bug caused data corruption.
21
+ class IntegrityError < Error; end
22
+
23
+ # Raised when ActiveStorage::Blob#download is called on a blob where the
24
+ # backing file is no longer present in its service.
25
+ class FileNotFoundError < Error; end
7
26
  end
@@ -7,10 +7,10 @@ module ActiveStorage
7
7
  end
8
8
 
9
9
  module VERSION
10
- MAJOR = 5
11
- MINOR = 2
12
- TINY = 4
13
- PRE = "4"
10
+ MAJOR = 6
11
+ MINOR = 1
12
+ TINY = 1
13
+ PRE = nil
14
14
 
15
15
  STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
16
16
  end
@@ -32,6 +32,12 @@ module ActiveStorage
32
32
  debug event, color("Generated URL for file at key: #{key_in(event)} (#{event.payload[:url]})", BLUE)
33
33
  end
34
34
 
35
+ def service_mirror(event)
36
+ message = "Mirrored file at key: #{key_in(event)}"
37
+ message += " (checksum: #{event.payload[:checksum]})" if event.payload[:checksum]
38
+ debug event, color(message, GREEN)
39
+ end
40
+
35
41
  def logger
36
42
  ActiveStorage.logger
37
43
  end
@@ -1,14 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "active_storage/downloading"
4
-
5
3
  module ActiveStorage
6
4
  # This is an abstract base class for previewers, which generate images from blobs. See
7
5
  # ActiveStorage::Previewer::MuPDFPreviewer and ActiveStorage::Previewer::VideoPreviewer for
8
6
  # examples of concrete subclasses.
9
7
  class Previewer
10
- include Downloading
11
-
12
8
  attr_reader :blob
13
9
 
14
10
  # Implement this method in a concrete subclass. Have it return true when given a blob from which
@@ -22,15 +18,21 @@ module ActiveStorage
22
18
  end
23
19
 
24
20
  # Override this method in a concrete subclass. Have it yield an attachable preview image (i.e.
25
- # anything accepted by ActiveStorage::Attached::One#attach).
26
- def preview
21
+ # anything accepted by ActiveStorage::Attached::One#attach). Pass the additional options to
22
+ # the underlying blob that is created.
23
+ def preview(**options)
27
24
  raise NotImplementedError
28
25
  end
29
26
 
30
27
  private
28
+ # Downloads the blob to a tempfile on disk. Yields the tempfile.
29
+ def download_blob_to_tempfile(&block) #:doc:
30
+ blob.open tmpdir: tmpdir, &block
31
+ end
32
+
31
33
  # Executes a system command, capturing its binary output in a tempfile. Yields the tempfile.
32
34
  #
33
- # Use this method to shell out to a system library (e.g. mupdf or ffmpeg) for preview image
35
+ # Use this method to shell out to a system library (e.g. muPDF or FFmpeg) for preview image
34
36
  # generation. The resulting tempfile can be used as the +:io+ value in an attachable Hash:
35
37
  #
36
38
  # def preview
@@ -41,18 +43,19 @@ module ActiveStorage
41
43
  # end
42
44
  # end
43
45
  #
44
- # The output tempfile is opened in the directory returned by ActiveStorage::Downloading#tempdir.
46
+ # The output tempfile is opened in the directory returned by #tmpdir.
45
47
  def draw(*argv) #:doc:
46
- ActiveSupport::Notifications.instrument("preview.active_storage") do
47
- open_tempfile_for_drawing do |file|
48
+ open_tempfile do |file|
49
+ instrument :preview, key: blob.key do
48
50
  capture(*argv, to: file)
49
- yield file
50
51
  end
52
+
53
+ yield file
51
54
  end
52
55
  end
53
56
 
54
- def open_tempfile_for_drawing
55
- tempfile = Tempfile.open("ActiveStorage", tempdir)
57
+ def open_tempfile
58
+ tempfile = Tempfile.open("ActiveStorage-", tmpdir)
56
59
 
57
60
  begin
58
61
  yield tempfile
@@ -61,6 +64,10 @@ module ActiveStorage
61
64
  end
62
65
  end
63
66
 
67
+ def instrument(operation, payload = {}, &block)
68
+ ActiveSupport::Notifications.instrument "#{operation}.active_storage", payload, &block
69
+ end
70
+
64
71
  def capture(*argv, to:)
65
72
  to.binmode
66
73
  IO.popen(argv, err: File::NULL) { |out| IO.copy_stream(out, to) }
@@ -70,5 +77,9 @@ module ActiveStorage
70
77
  def logger #:doc:
71
78
  ActiveStorage.logger
72
79
  end
80
+
81
+ def tmpdir #:doc:
82
+ Dir.tmpdir
83
+ end
73
84
  end
74
85
  end
@@ -12,7 +12,7 @@ module ActiveStorage
12
12
  end
13
13
 
14
14
  def mutool_exists?
15
- return @mutool_exists unless @mutool_exists.nil?
15
+ return @mutool_exists if defined?(@mutool_exists) && !@mutool_exists.nil?
16
16
 
17
17
  system mutool_path, out: File::NULL, err: File::NULL
18
18
 
@@ -20,10 +20,10 @@ module ActiveStorage
20
20
  end
21
21
  end
22
22
 
23
- def preview
23
+ def preview(**options)
24
24
  download_blob_to_tempfile do |input|
25
25
  draw_first_page_from input do |output|
26
- yield io: output, filename: "#{blob.filename.base}.png", content_type: "image/png"
26
+ yield io: output, filename: "#{blob.filename.base}.png", content_type: "image/png", **options
27
27
  end
28
28
  end
29
29
  end
@@ -12,24 +12,24 @@ module ActiveStorage
12
12
  end
13
13
 
14
14
  def pdftoppm_exists?
15
- return @pdftoppm_exists unless @pdftoppm_exists.nil?
15
+ return @pdftoppm_exists if defined?(@pdftoppm_exists)
16
16
 
17
17
  @pdftoppm_exists = system(pdftoppm_path, "-v", out: File::NULL, err: File::NULL)
18
18
  end
19
19
  end
20
20
 
21
- def preview
21
+ def preview(**options)
22
22
  download_blob_to_tempfile do |input|
23
23
  draw_first_page_from input do |output|
24
- yield io: output, filename: "#{blob.filename.base}.png", content_type: "image/png"
24
+ yield io: output, filename: "#{blob.filename.base}.png", content_type: "image/png", **options
25
25
  end
26
26
  end
27
27
  end
28
28
 
29
29
  private
30
30
  def draw_first_page_from(file, &block)
31
- # use 72 dpi to match thumbnail dimesions of the PDF
32
- draw self.class.pdftoppm_path, "-singlefile", "-r", "72", "-png", file.path, &block
31
+ # use 72 dpi to match thumbnail dimensions of the PDF
32
+ draw self.class.pdftoppm_path, "-singlefile", "-cropbox", "-r", "72", "-png", file.path, &block
33
33
  end
34
34
  end
35
35
  end