Skip to content

Commit 1f16768

Browse files
authored
Merge pull request rails#42005 from FestaLab/activestorage/vips-analyzer
Add support for vips in the image analyzer
2 parents 46fcce3 + 82fdf45 commit 1f16768

File tree

11 files changed

+215
-75
lines changed

11 files changed

+215
-75
lines changed

activestorage/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
* Use libvips instead of ImageMagick to analyze images when `active_storage.variant_processor = vips`
2+
3+
*Breno Gazzola*
4+
15
* Add metadata value for presence of video channel in video blobs
26

37
The `metadata` attribute of video blobs has a new boolean key named `video` that is set to

activestorage/lib/active_storage/analyzer.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
module ActiveStorage
44
# This is an abstract base class for analyzers, which extract metadata from blobs. See
5-
# ActiveStorage::Analyzer::ImageAnalyzer for an example of a concrete subclass.
5+
# ActiveStorage::Analyzer::VideoAnalyzer for an example of a concrete subclass.
66
class Analyzer
77
attr_reader :blob
88

Lines changed: 2 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,14 @@
11
# frozen_string_literal: true
22

33
module ActiveStorage
4-
# Extracts width and height in pixels from an image blob.
4+
# This is an abstract base class for image analyzers, which extract width and height from an image blob.
55
#
66
# If the image contains EXIF data indicating its angle is 90 or 270 degrees, its width and height are swapped for convenience.
77
#
88
# Example:
99
#
10-
# ActiveStorage::Analyzer::ImageAnalyzer.new(blob).metadata
10+
# ActiveStorage::Analyzer::ImageAnalyzer::ImageMagick.new(blob).metadata
1111
# # => { width: 4104, height: 2736 }
12-
#
13-
# This analyzer relies on the third-party {MiniMagick}[https://github.com/minimagick/minimagick] gem. MiniMagick requires
14-
# the {ImageMagick}[http://www.imagemagick.org] system library.
1512
class Analyzer::ImageAnalyzer < Analyzer
1613
def self.accept?(blob)
1714
blob.image?
@@ -26,30 +23,5 @@ def metadata
2623
end
2724
end
2825
end
29-
30-
private
31-
def read_image
32-
download_blob_to_tempfile do |file|
33-
require "mini_magick"
34-
image = MiniMagick::Image.new(file.path)
35-
36-
if image.valid?
37-
yield image
38-
else
39-
logger.info "Skipping image analysis because ImageMagick doesn't support the file"
40-
{}
41-
end
42-
end
43-
rescue LoadError
44-
logger.info "Skipping image analysis because the mini_magick gem isn't installed"
45-
{}
46-
rescue MiniMagick::Error => error
47-
logger.error "Skipping image analysis due to an ImageMagick error: #{error.message}"
48-
{}
49-
end
50-
51-
def rotated_image?(image)
52-
%w[ RightTop LeftBottom ].include?(image["%[orientation]"])
53-
end
5426
end
5527
end
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# frozen_string_literal: true
2+
3+
module ActiveStorage
4+
# This analyzer relies on the third-party {MiniMagick}[https://github.com/minimagick/minimagick] gem. MiniMagick requires
5+
# the {ImageMagick}[http://www.imagemagick.org] system library.
6+
class Analyzer::ImageAnalyzer::ImageMagick < Analyzer::ImageAnalyzer
7+
def self.accept?(blob)
8+
super && ActiveStorage.variant_processor == :mini_magick
9+
end
10+
11+
private
12+
def read_image
13+
download_blob_to_tempfile do |file|
14+
require "mini_magick"
15+
image = MiniMagick::Image.new(file.path)
16+
17+
if image.valid?
18+
yield image
19+
else
20+
logger.info "Skipping image analysis because ImageMagick doesn't support the file"
21+
{}
22+
end
23+
end
24+
rescue LoadError
25+
logger.info "Skipping image analysis because the mini_magick gem isn't installed"
26+
{}
27+
rescue MiniMagick::Error => error
28+
logger.error "Skipping image analysis due to an ImageMagick error: #{error.message}"
29+
{}
30+
end
31+
32+
def rotated_image?(image)
33+
%w[ RightTop LeftBottom TopRight BottomLeft ].include?(image["%[orientation]"])
34+
end
35+
end
36+
end
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# frozen_string_literal: true
2+
3+
module ActiveStorage
4+
# This analyzer relies on the third-party {ruby-vips}[https://github.com/libvips/ruby-vips] gem. Ruby-vips requires
5+
# the {libvips}[https://libvips.github.io/libvips/] system library.
6+
class Analyzer::ImageAnalyzer::Vips < Analyzer::ImageAnalyzer
7+
def self.accept?(blob)
8+
super && ActiveStorage.variant_processor == :vips
9+
end
10+
11+
private
12+
def read_image
13+
download_blob_to_tempfile do |file|
14+
require "ruby-vips"
15+
image = ::Vips::Image.new_from_file(file.path, access: :sequential)
16+
17+
if valid_image?(image)
18+
yield image
19+
else
20+
logger.info "Skipping image analysis because Vips doesn't support the file"
21+
{}
22+
end
23+
end
24+
rescue LoadError
25+
logger.info "Skipping image analysis because the ruby-vips gem isn't installed"
26+
{}
27+
rescue ::Vips::Error => error
28+
logger.error "Skipping image analysis due to an Vips error: #{error.message}"
29+
{}
30+
end
31+
32+
ROTATIONS = /Right-top|Left-bottom|Top-right|Bottom-left/
33+
def rotated_image?(image)
34+
ROTATIONS === image.get("exif-ifd0-Orientation")
35+
rescue ::Vips::Error
36+
false
37+
end
38+
39+
def valid_image?(image)
40+
image.avg
41+
true
42+
rescue ::Vips::Error
43+
false
44+
end
45+
end
46+
end

activestorage/lib/active_storage/engine.rb

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
require "active_storage/previewer/video_previewer"
1313

1414
require "active_storage/analyzer/image_analyzer"
15+
require "active_storage/analyzer/image_analyzer/image_magick"
16+
require "active_storage/analyzer/image_analyzer/vips"
1517
require "active_storage/analyzer/video_analyzer"
1618
require "active_storage/analyzer/audio_analyzer"
1719

@@ -25,7 +27,7 @@ class Engine < Rails::Engine # :nodoc:
2527

2628
config.active_storage = ActiveSupport::OrderedOptions.new
2729
config.active_storage.previewers = [ ActiveStorage::Previewer::PopplerPDFPreviewer, ActiveStorage::Previewer::MuPDFPreviewer, ActiveStorage::Previewer::VideoPreviewer ]
28-
config.active_storage.analyzers = [ ActiveStorage::Analyzer::ImageAnalyzer, ActiveStorage::Analyzer::VideoAnalyzer, ActiveStorage::Analyzer::AudioAnalyzer ]
30+
config.active_storage.analyzers = [ ActiveStorage::Analyzer::ImageAnalyzer::Vips, ActiveStorage::Analyzer::ImageAnalyzer::ImageMagick, ActiveStorage::Analyzer::VideoAnalyzer, ActiveStorage::Analyzer::AudioAnalyzer ]
2931
config.active_storage.paths = ActiveSupport::OrderedOptions.new
3032
config.active_storage.queues = ActiveSupport::InheritableOptions.new
3133

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
# frozen_string_literal: true
2+
3+
require "test_helper"
4+
require "database/setup"
5+
6+
require "active_storage/analyzer/image_analyzer"
7+
8+
class ActiveStorage::Analyzer::ImageAnalyzer::ImageMagickTest < ActiveSupport::TestCase
9+
test "analyzing a JPEG image" do
10+
analyze_with_image_magick do
11+
blob = create_file_blob(filename: "racecar.jpg", content_type: "image/jpeg")
12+
metadata = extract_metadata_from(blob)
13+
14+
assert_equal 4104, metadata[:width]
15+
assert_equal 2736, metadata[:height]
16+
end
17+
end
18+
19+
test "analyzing a rotated JPEG image" do
20+
analyze_with_image_magick do
21+
blob = create_file_blob(filename: "racecar_rotated.jpg", content_type: "image/jpeg")
22+
metadata = extract_metadata_from(blob)
23+
24+
assert_equal 2736, metadata[:width]
25+
assert_equal 4104, metadata[:height]
26+
end
27+
end
28+
29+
test "analyzing an SVG image without an XML declaration" do
30+
analyze_with_image_magick do
31+
blob = create_file_blob(filename: "icon.svg", content_type: "image/svg+xml")
32+
metadata = extract_metadata_from(blob)
33+
34+
assert_equal 792, metadata[:width]
35+
assert_equal 584, metadata[:height]
36+
end
37+
end
38+
39+
test "analyzing an unsupported image type" do
40+
analyze_with_image_magick do
41+
blob = create_blob(data: "bad", filename: "bad_file.bad", content_type: "image/bad_type")
42+
metadata = extract_metadata_from(blob)
43+
44+
assert_nil metadata[:width]
45+
assert_nil metadata[:height]
46+
end
47+
end
48+
49+
private
50+
def analyze_with_image_magick
51+
previous_processor, ActiveStorage.variant_processor = ActiveStorage.variant_processor, :mini_magick
52+
require "mini_magick"
53+
54+
yield
55+
rescue LoadError
56+
ENV["CI"] ? raise : skip("Variant processor image_magick is not installed")
57+
ensure
58+
ActiveStorage.variant_processor = previous_processor
59+
end
60+
end
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
# frozen_string_literal: true
2+
3+
require "test_helper"
4+
require "database/setup"
5+
6+
require "active_storage/analyzer/image_analyzer"
7+
8+
class ActiveStorage::Analyzer::ImageAnalyzer::VipsTest < ActiveSupport::TestCase
9+
test "analyzing a JPEG image" do
10+
analyze_with_vips do
11+
blob = create_file_blob(filename: "racecar.jpg", content_type: "image/jpeg")
12+
metadata = extract_metadata_from(blob)
13+
14+
assert_equal 4104, metadata[:width]
15+
assert_equal 2736, metadata[:height]
16+
end
17+
end
18+
19+
test "analyzing a rotated JPEG image" do
20+
analyze_with_vips do
21+
blob = create_file_blob(filename: "racecar_rotated.jpg", content_type: "image/jpeg")
22+
metadata = extract_metadata_from(blob)
23+
24+
assert_equal 2736, metadata[:width]
25+
assert_equal 4104, metadata[:height]
26+
end
27+
end
28+
29+
test "analyzing an SVG image without an XML declaration" do
30+
analyze_with_vips do
31+
blob = create_file_blob(filename: "icon.svg", content_type: "image/svg+xml")
32+
metadata = extract_metadata_from(blob)
33+
34+
assert_equal 792, metadata[:width]
35+
assert_equal 584, metadata[:height]
36+
end
37+
end
38+
39+
test "analyzing an unsupported image type" do
40+
analyze_with_vips do
41+
blob = create_blob(data: "bad", filename: "bad_file.bad", content_type: "image/bad_type")
42+
metadata = extract_metadata_from(blob)
43+
44+
assert_nil metadata[:width]
45+
assert_nil metadata[:height]
46+
end
47+
end
48+
49+
private
50+
def analyze_with_vips
51+
previous_processor, ActiveStorage.variant_processor = ActiveStorage.variant_processor, :vips
52+
require "ruby-vips"
53+
54+
yield
55+
rescue LoadError
56+
ENV["CI"] ? raise : skip("Variant processor vips is not installed")
57+
ensure
58+
ActiveStorage.variant_processor = previous_processor
59+
end
60+
end

activestorage/test/analyzer/image_analyzer_test.rb

Lines changed: 0 additions & 40 deletions
This file was deleted.

activestorage/test/models/variant_test.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,7 @@ def process_variants_with(processor)
206206
previous_processor, ActiveStorage.variant_processor = ActiveStorage.variant_processor, processor
207207
yield
208208
rescue LoadError
209-
skip "Variant processor #{processor.inspect} is not installed"
209+
ENV["CI"] ? raise : skip("Variant processor #{processor.inspect} is not installed")
210210
ensure
211211
ActiveStorage.variant_processor = previous_processor
212212
end

0 commit comments

Comments
 (0)