diff options
Diffstat (limited to 'activestorage/app/models')
5 files changed, 91 insertions, 18 deletions
diff --git a/activestorage/app/models/active_storage/blob.rb b/activestorage/app/models/active_storage/blob.rb index 2aa05d665e..3b48ee72af 100644 --- a/activestorage/app/models/active_storage/blob.rb +++ b/activestorage/app/models/active_storage/blob.rb @@ -16,6 +16,7 @@ require "active_storage/analyzer/null_analyzer" # update a blob's metadata on a subsequent pass, but you should not update the key or change the uploaded file. # If you need to create a derivative or otherwise change the blob, simply create a new blob and purge the old one. class ActiveStorage::Blob < ActiveRecord::Base + class InvariableError < StandardError; end class UnpreviewableError < StandardError; end class UnrepresentableError < StandardError; end @@ -110,6 +111,7 @@ class ActiveStorage::Blob < ActiveRecord::Base content_type.start_with?("text") end + # Returns an ActiveStorage::Variant instance with the set of +transformations+ provided. This is only relevant for image # files, and it allows any image to be transformed for size, colors, and the like. Example: # @@ -125,8 +127,20 @@ class ActiveStorage::Blob < ActiveRecord::Base # # This will create a URL for that specific blob with that specific variant, which the ActiveStorage::VariantsController # can then produce on-demand. + # + # Raises ActiveStorage::Blob::InvariableError if ImageMagick cannot transform the blob. To determine whether a blob is + # variable, call ActiveStorage::Blob#previewable?. def variant(transformations) - ActiveStorage::Variant.new(self, ActiveStorage::Variation.wrap(transformations)) + if variable? + ActiveStorage::Variant.new(self, ActiveStorage::Variation.wrap(transformations)) + else + raise InvariableError + end + end + + # Returns true if ImageMagick can transform the blob (its content type is in +ActiveStorage.variable_content_types+). + def variable? + ActiveStorage.variable_content_types.include?(content_type) end @@ -158,11 +172,11 @@ class ActiveStorage::Blob < ActiveRecord::Base end - # Returns an ActiveStorage::Preview instance for a previewable blob or an ActiveStorage::Variant instance for an image blob. + # Returns an ActiveStorage::Preview for a previewable blob or an ActiveStorage::Variant for a variable image blob. # # blob.representation(resize: "100x100").processed.service_url # - # Raises ActiveStorage::Blob::UnrepresentableError if the receiving blob is neither an image nor previewable. Call + # Raises ActiveStorage::Blob::UnrepresentableError if the receiving blob is neither variable nor previewable. Call # ActiveStorage::Blob#representable? to determine whether a blob is representable. # # See ActiveStorage::Blob#preview and ActiveStorage::Blob#variant for more information. @@ -170,16 +184,16 @@ class ActiveStorage::Blob < ActiveRecord::Base case when previewable? preview transformations - when image? + when variable? variant transformations else raise UnrepresentableError end end - # Returns true if the blob is an image or is previewable. + # Returns true if the blob is variable or previewable. def representable? - image? || previewable? + variable? || previewable? end @@ -270,7 +284,8 @@ class ActiveStorage::Blob < ActiveRecord::Base # deleted as well or you will essentially have a dead reference. It's recommended to use the +#purge+ and +#purge_later+ # methods in most circumstances. def delete - service.delete key + service.delete(key) + service.delete_prefixed("variants/#{key}/") if image? end # Deletes the file on the service and then destroys the blob record. This is the recommended way to dispose of unwanted diff --git a/activestorage/app/models/active_storage/filename.rb b/activestorage/app/models/active_storage/filename.rb index 79d55dc889..b9413dec95 100644 --- a/activestorage/app/models/active_storage/filename.rb +++ b/activestorage/app/models/active_storage/filename.rb @@ -50,7 +50,7 @@ class ActiveStorage::Filename @filename.encode(Encoding::UTF_8, invalid: :replace, undef: :replace, replace: "�").strip.tr("\u{202E}%$|:;/\t\r\n\\", "-") end - def parameters + def parameters #:nodoc: Parameters.new self end diff --git a/activestorage/app/models/active_storage/filename/parameters.rb b/activestorage/app/models/active_storage/filename/parameters.rb index 58ce198d38..fb9ea10e49 100644 --- a/activestorage/app/models/active_storage/filename/parameters.rb +++ b/activestorage/app/models/active_storage/filename/parameters.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -class ActiveStorage::Filename::Parameters +class ActiveStorage::Filename::Parameters #:nodoc: attr_reader :filename def initialize(filename) diff --git a/activestorage/app/models/active_storage/variant.rb b/activestorage/app/models/active_storage/variant.rb index fa5aa69bd3..e08a2271ec 100644 --- a/activestorage/app/models/active_storage/variant.rb +++ b/activestorage/app/models/active_storage/variant.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require "active_storage/downloading" + # Image blobs can have variants that are the result of a set of transformations applied to the original. # These variants are used to create thumbnails, fixed-size avatars, or any other derivative image from the # original. @@ -16,7 +18,7 @@ # To refer to such a delayed on-demand variant, simply link to the variant through the resolved route provided # by Active Storage like so: # -# <%= image_tag url_for(Current.user.avatar.variant(resize: "100x100")) %> +# <%= image_tag Current.user.avatar.variant(resize: "100x100") %> # # This will create a URL for that specific blob with that specific variant, which the ActiveStorage::VariantsController # can then produce on-demand. @@ -35,6 +37,10 @@ # # avatar.variant(resize: "100x100", monochrome: true, flip: "-90") class ActiveStorage::Variant + include ActiveStorage::Downloading + + WEB_IMAGE_CONTENT_TYPES = %w( image/png image/jpeg image/jpg image/gif ) + attr_reader :blob, :variation delegate :service, to: :blob @@ -62,7 +68,7 @@ class ActiveStorage::Variant # for a variant that points to the ActiveStorage::VariantsController, which in turn will use this +service_call+ method # for its redirection. def service_url(expires_in: service.url_expires_in, disposition: :inline) - service.url key, expires_in: expires_in, disposition: disposition, filename: blob.filename, content_type: blob.content_type + service.url key, expires_in: expires_in, disposition: disposition, filename: filename, content_type: content_type end # Returns the receiving variant. Allows ActiveStorage::Variant and ActiveStorage::Preview instances to be used interchangeably. @@ -76,11 +82,51 @@ class ActiveStorage::Variant end def process - service.upload key, transform(service.download(blob.key)) + open_image do |image| + transform image + format image + upload image + end + end + + + def filename + if WEB_IMAGE_CONTENT_TYPES.include?(blob.content_type) + blob.filename + else + ActiveStorage::Filename.new("#{blob.filename.base}.png") + end + end + + def content_type + blob.content_type.presence_in(WEB_IMAGE_CONTENT_TYPES) || "image/png" + end + + + def open_image(&block) + image = download_image + + begin + yield image + ensure + image.destroy! + end end - def transform(io) + def download_image require "mini_magick" - File.open MiniMagick::Image.read(io).tap { |image| variation.transform(image) }.path + MiniMagick::Image.create { |file| download_blob_to(file) } + end + + def transform(image) + variation.transform(image) + end + + def format(image) + image.format("PNG") unless WEB_IMAGE_CONTENT_TYPES.include?(blob.content_type) + end + + def upload(image) + File.open(image.path, "r") { |file| service.upload(key, file) } end end diff --git a/activestorage/app/models/active_storage/variation.rb b/activestorage/app/models/active_storage/variation.rb index 13bad87cac..2b599a4710 100644 --- a/activestorage/app/models/active_storage/variation.rb +++ b/activestorage/app/models/active_storage/variation.rb @@ -46,11 +46,15 @@ class ActiveStorage::Variation # Accepts an open MiniMagick image instance, like what's returned by <tt>MiniMagick::Image.read(io)</tt>, # and performs the +transformations+ against it. The transformed image instance is then returned. def transform(image) - transformations.each do |(method, argument)| - if eligible_argument?(argument) - image.public_send(method, argument) + transformations.each do |(transformation_method, transformation_argument)| + if transformation_method.to_s == "combine_options" + image.combine_options do |combination| + transformation_argument.each do |(method, argument)| + pass_transform_argument(combination, method, argument) + end + end else - image.public_send(method) + pass_transform_argument(image, transformation_method, transformation_argument) end end end @@ -64,4 +68,12 @@ class ActiveStorage::Variation def eligible_argument?(argument) argument.present? && argument != true end + + def pass_transform_argument(instance, method, argument) + if eligible_argument?(argument) + instance.public_send(method, argument) + else + instance.public_send(method) + end + end end |