-
-
Notifications
You must be signed in to change notification settings - Fork 133
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[Analyzer] Refacto our image analyzers to further expand the gem (#254)
- Loading branch information
Showing
29 changed files
with
613 additions
and
263 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
# frozen_string_literal: true | ||
|
||
require_relative 'shared/asv_attachable' | ||
require_relative 'shared/asv_loggable' | ||
|
||
module ActiveStorageValidations | ||
# = Active Storage Validations \Analyzer | ||
# | ||
# This is an abstract base class for analyzers, which extract metadata from attachables. | ||
# See ActiveStorageValidations::Analyzer::ImageAnalyzer for an example of a concrete subclass. | ||
# | ||
# Heavily (not to say 100%) inspired by Rails own ActiveStorage::Analyzer | ||
class Analyzer | ||
include ASVAttachable | ||
include ASVLoggable | ||
|
||
attr_reader :attachable | ||
|
||
# Implement this method in a concrete subclass. Have it return true when given an attachable from which | ||
# the analyzer can extract metadata. | ||
def self.accept?(attachable) | ||
false | ||
end | ||
|
||
# Returns true if the attachable media_type matches, like image?(attachable) returns | ||
# true for 'image/png' | ||
class << self | ||
%w[ | ||
image | ||
audio | ||
video | ||
].each do |media_type| | ||
define_method(:"#{media_type}?") do |attachable| | ||
attachable_content_type(attachable).start_with?(media_type) | ||
end | ||
end | ||
|
||
def attachable_content_type(attachable) | ||
new(attachable).send(:attachable_content_type, attachable) | ||
end | ||
end | ||
|
||
def initialize(attachable) | ||
@attachable = attachable | ||
end | ||
|
||
# Override this method in a concrete subclass. Have it return a Hash of metadata. | ||
def metadata | ||
raise NotImplementedError | ||
end | ||
|
||
private | ||
|
||
def instrument(analyzer, &block) | ||
ActiveSupport::Notifications.instrument("analyze.active_storage_validations", analyzer: analyzer, &block) | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,81 @@ | ||
# frozen_string_literal: true | ||
|
||
module ActiveStorageValidations | ||
# = Active Storage Image \Analyzer | ||
# | ||
# This is an abstract base class for image analyzers, which extract width and height from an image attachable. | ||
# | ||
# If the image contains EXIF data indicating its angle is 90 or 270 degrees, its width and height are swapped for convenience. | ||
# | ||
# Example: | ||
# | ||
# ActiveStorage::Analyzer::ImageAnalyzer::ImageMagick.new(attachable).metadata | ||
# # => { width: 4104, height: 2736 } | ||
class Analyzer::ImageAnalyzer < Analyzer | ||
def self.accept?(attachable) | ||
image?(attachable) | ||
end | ||
|
||
def metadata | ||
read_image do |image| | ||
if rotated_image?(image) | ||
{ width: image.height, height: image.width } | ||
else | ||
{ width: image.width, height: image.height } | ||
end | ||
end | ||
end | ||
|
||
private | ||
|
||
def image | ||
case @attachable | ||
when ActiveStorage::Blob, String | ||
blob = @attachable.is_a?(String) ? ActiveStorage::Blob.find_signed!(@attachable) : @attachable | ||
tempfile = tempfile_from_blob(blob) | ||
|
||
image_from_path(tempfile.path) | ||
when Hash | ||
io = @attachable[:io] | ||
file = io.is_a?(StringIO) ? tempfile_from_io(io) : File.open(io) | ||
|
||
image_from_path(file.path) | ||
when ActionDispatch::Http::UploadedFile, Rack::Test::UploadedFile, File | ||
image_from_path(@attachable.path) | ||
when Pathname | ||
image_from_path(@attachable.to_s) | ||
else | ||
raise_rails_like_error(@attachable) | ||
end | ||
end | ||
|
||
def tempfile_from_blob(blob) | ||
tempfile = Tempfile.new(["ActiveStorage-#{blob.id}-", blob.filename.extension_with_delimiter], binmode: true) | ||
|
||
blob.download { |chunk| tempfile.write(chunk) } | ||
|
||
tempfile.flush | ||
tempfile.rewind | ||
tempfile | ||
end | ||
|
||
def tempfile_from_io(io) | ||
tempfile = Tempfile.new([File.basename(@attachable[:filename], '.*'), File.extname(@attachable[:filename])], binmode: true) | ||
|
||
IO.copy_stream(io, tempfile) | ||
io.rewind | ||
|
||
tempfile.flush | ||
tempfile.rewind | ||
tempfile | ||
end | ||
|
||
def image_from_path(path) | ||
raise NotImplementedError | ||
end | ||
|
||
def rotated_image?(image) | ||
raise NotImplementedError | ||
end | ||
end | ||
end |
40 changes: 40 additions & 0 deletions
40
lib/active_storage_validations/analyzer/image_analyzer/image_magick.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
# frozen_string_literal: true | ||
|
||
module ActiveStorageValidations | ||
# This analyzer relies on the third-party {MiniMagick}[https://github.com/minimagick/minimagick] gem. | ||
# MiniMagick requires the {ImageMagick}[http://www.imagemagick.org] system library. | ||
# This is the default Rails image analyzer. | ||
class Analyzer::ImageAnalyzer::ImageMagick < Analyzer::ImageAnalyzer | ||
|
||
private | ||
|
||
def read_image | ||
begin | ||
require "mini_magick" | ||
rescue LoadError | ||
logger.info "Skipping image analysis because the mini_magick gem isn't installed" | ||
return {} | ||
end | ||
|
||
if image.valid? | ||
yield image | ||
else | ||
logger.info "Skipping image analysis because ImageMagick doesn't support the file" | ||
{} | ||
end | ||
rescue MiniMagick::Error => error | ||
logger.error "Skipping image analysis due to an ImageMagick error: #{error.message}" | ||
{} | ||
end | ||
|
||
def image_from_path(path) | ||
instrument("mini_magick") do | ||
MiniMagick::Image.new(path) | ||
end | ||
end | ||
|
||
def rotated_image?(image) | ||
%w[ RightTop LeftBottom TopRight BottomLeft ].include?(image["%[orientation]"]) | ||
end | ||
end | ||
end |
51 changes: 51 additions & 0 deletions
51
lib/active_storage_validations/analyzer/image_analyzer/vips.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
# frozen_string_literal: true | ||
|
||
module ActiveStorageValidations | ||
# This analyzer relies on the third-party {ruby-vips}[https://github.com/libvips/ruby-vips] gem. | ||
# Ruby-vips requires the {libvips}[https://libvips.github.io/libvips/] system library. | ||
class Analyzer::ImageAnalyzer::Vips < Analyzer::ImageAnalyzer | ||
|
||
private | ||
|
||
def read_image | ||
begin | ||
require "ruby-vips" | ||
rescue LoadError | ||
logger.info "Skipping image analysis because the ruby-vips gem isn't installed" | ||
return {} | ||
end | ||
|
||
if image | ||
yield image | ||
else | ||
logger.info "Skipping image analysis because Vips doesn't support the file" | ||
{} | ||
end | ||
rescue ::Vips::Error => error | ||
logger.error "Skipping image analysis due to a Vips error: #{error.message}" | ||
{} | ||
end | ||
|
||
def image_from_path(path) | ||
instrument("vips") do | ||
begin | ||
::Vips::Image.new_from_file(path, access: :sequential) | ||
rescue ::Vips::Error | ||
# Vips throw errors rather than returning false when reading a not | ||
# supported attachable. | ||
# We stumbled upon this issue while reading 0 byte size attachable | ||
# https://github.com/janko/image_processing/issues/97 | ||
logger.info "Skipping image analysis because Vips doesn't support the file" | ||
nil | ||
end | ||
end | ||
end | ||
|
||
ROTATIONS = /Right-top|Left-bottom|Top-right|Bottom-left/ | ||
def rotated_image?(image) | ||
ROTATIONS === image.get("exif-ifd0-Orientation") | ||
rescue ::Vips::Error | ||
false | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
# frozen_string_literal: true | ||
|
||
module ActiveStorageValidations | ||
# = Active Storage Null Analyzer | ||
# | ||
# This is a fallback analyzer when the attachable media type is not supported | ||
# by our gem. | ||
# | ||
# Example: | ||
# | ||
# ActiveStorage::Analyzer::NullAnalyzer.new(attachable).metadata | ||
# # => {} | ||
class Analyzer::NullAnalyzer < Analyzer | ||
def self.accept?(attachable) | ||
true | ||
end | ||
|
||
def metadata | ||
{} | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.