active_analysis 0.1.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/CHANGELOG.md +5 -0
- data/MIT-LICENSE +20 -0
- data/README.md +43 -0
- data/Rakefile +19 -0
- data/app/assets/config/active_analysis_manifest.js +1 -0
- data/app/assets/stylesheets/active_analysis/application.css +15 -0
- data/app/controllers/active_analysis/application_controller.rb +4 -0
- data/app/helpers/active_analysis/application_helper.rb +4 -0
- data/app/jobs/active_analysis/application_job.rb +4 -0
- data/app/mailers/active_analysis/application_mailer.rb +6 -0
- data/app/models/active_analysis/application_record.rb +5 -0
- data/app/views/layouts/active_analysis/application.html.erb +15 -0
- data/config/routes.rb +2 -0
- data/lib/active_analysis.rb +12 -0
- data/lib/active_analysis/analyzer.rb +8 -0
- data/lib/active_analysis/analyzer/audio_analyzer.rb +64 -0
- data/lib/active_analysis/analyzer/image_analyzer.rb +28 -0
- data/lib/active_analysis/analyzer/image_analyzer/image_magick.rb +37 -0
- data/lib/active_analysis/analyzer/image_analyzer/vips.rb +48 -0
- data/lib/active_analysis/analyzer/pdf_analyzer.rb +81 -0
- data/lib/active_analysis/analyzer/video_analyzer.rb +141 -0
- data/lib/active_analysis/engine.rb +45 -0
- data/lib/active_analysis/gem_version.rb +17 -0
- data/lib/active_analysis/version.rb +10 -0
- data/lib/tasks/active_analysis_tasks.rake +4 -0
- metadata +226 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 2264c818bb6bbdf31ecde9797cbeb77edf1f42b52d3b7f3f0178fbf47fec2ef8
|
4
|
+
data.tar.gz: 8cf368194ab3226945d275fea222685b346c6c6821dddde0ad9ddcdea40ca445
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: e9381a23b549ef234a7d4e13aa04460b99d6365b1f82f6cd3359d8d4b31e72d7be3664ee64536ff73429353c83122f5a0f113b64ebd104c530f1721cb1ed363e
|
7
|
+
data.tar.gz: c9f3f4ce840c7b1cfae253459f09af28163672fff60c1d0643f45f256d3b08d4689752de359d47923e883c1e43abb9a6e33a91c8089ea27a26b7e95a72dc5710
|
data/CHANGELOG.md
ADDED
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright 2021 Breno Gazzola
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
# Active Analysis
|
2
|
+
|
3
|
+
A collection of active storage analyzers
|
4
|
+
|
5
|
+
[![active-analysis-main](https://github.com/FestaLab/active_analysis/actions/workflows/main.yml/badge.svg)](https://github.com/FestaLab/active_analysis/actions/workflows/main.yml)
|
6
|
+
|
7
|
+
## Installation
|
8
|
+
|
9
|
+
Add this line to your application's Gemfile:
|
10
|
+
|
11
|
+
```ruby
|
12
|
+
gem 'active_analysis'
|
13
|
+
```
|
14
|
+
|
15
|
+
And then execute:
|
16
|
+
|
17
|
+
$ bundle install
|
18
|
+
|
19
|
+
## Usage
|
20
|
+
|
21
|
+
Active Analyzers will automatically add all its analyzers to Active Storage's analyzers array. Don't worry, if you added any custom analyzers they will be there. You can check the comments in the source code of each analyzer to see what information they extract from files:
|
22
|
+
- Audio: [source](https://github.com/FestaLab/active_analysis/blob/main/lib/active_analysis/analyzer/audio_analyzer.rb)
|
23
|
+
- Image: [source](https://github.com/FestaLab/active_analysis/blob/main/lib/active_analysis/analyzer/image_analyzer.rb)
|
24
|
+
- PDF: [source](https://github.com/FestaLab/active_analysis/blob/main/lib/active_analysis/analyzer/pdf_analyzer.rb)
|
25
|
+
- Video: [source](https://github.com/FestaLab/active_analysis/blob/main/lib/active_analysis/analyzer/video_analyzer.rb)
|
26
|
+
|
27
|
+
## Development
|
28
|
+
|
29
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
30
|
+
|
31
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
32
|
+
|
33
|
+
## Contributing
|
34
|
+
|
35
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/FestaLab/active_analysis. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/FestaLab/active_analysis/blob/main/CODE_OF_CONDUCT.md).
|
36
|
+
|
37
|
+
## License
|
38
|
+
|
39
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
40
|
+
|
41
|
+
## Code of Conduct
|
42
|
+
|
43
|
+
Everyone interacting in the ActiveAnalysis project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/FestaLab/active_analysis/blob/main/CODE_OF_CONDUCT.md).
|
data/Rakefile
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
|
5
|
+
APP_RAKEFILE = File.expand_path("test/dummy/Rakefile", __dir__)
|
6
|
+
|
7
|
+
load "rails/tasks/engine.rake"
|
8
|
+
load "rails/tasks/statistics.rake"
|
9
|
+
|
10
|
+
require "bundler/gem_tasks"
|
11
|
+
require "rake/testtask"
|
12
|
+
|
13
|
+
Rake::TestTask.new(:test) do |t|
|
14
|
+
t.libs << "test"
|
15
|
+
t.pattern = "test/**/*_test.rb"
|
16
|
+
t.verbose = false
|
17
|
+
end
|
18
|
+
|
19
|
+
task default: :test
|
@@ -0,0 +1 @@
|
|
1
|
+
//= link_directory ../stylesheets/active_analysis .css
|
@@ -0,0 +1,15 @@
|
|
1
|
+
/*
|
2
|
+
* This is a manifest file that'll be compiled into application.css, which will include all the files
|
3
|
+
* listed below.
|
4
|
+
*
|
5
|
+
* Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
|
6
|
+
* or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path.
|
7
|
+
*
|
8
|
+
* You're free to add application-wide styles to this file and they'll appear at the bottom of the
|
9
|
+
* compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
|
10
|
+
* files in this directory. Styles in this file should be added after the last require_* statement.
|
11
|
+
* It is generally better to create a new file per style scope.
|
12
|
+
*
|
13
|
+
*= require_tree .
|
14
|
+
*= require_self
|
15
|
+
*/
|
@@ -0,0 +1,15 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html>
|
3
|
+
<head>
|
4
|
+
<title>Active analysis</title>
|
5
|
+
<%= csrf_meta_tags %>
|
6
|
+
<%= csp_meta_tag %>
|
7
|
+
|
8
|
+
<%= stylesheet_link_tag "active_analysis/application", media: "all" %>
|
9
|
+
</head>
|
10
|
+
<body>
|
11
|
+
|
12
|
+
<%= yield %>
|
13
|
+
|
14
|
+
</body>
|
15
|
+
</html>
|
data/config/routes.rb
ADDED
@@ -0,0 +1,8 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveAnalysis
|
4
|
+
# This is an abstract base class for analyzers, which extract metadata from blobs. See
|
5
|
+
# Analyzer::AudioAnalyzer for an example of a concrete subclass.
|
6
|
+
class Analyzer < ActiveStorage::Analyzer
|
7
|
+
end
|
8
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveAnalysis
|
4
|
+
# Extracts duration (seconds) and bit_rate (bits/s) from an audio blob.
|
5
|
+
#
|
6
|
+
# Example:
|
7
|
+
#
|
8
|
+
# ActiveStorage::Analyzer::AudioAnalyzer.new(blob).metadata
|
9
|
+
# # => { duration: 5.0, bit_rate: 320340 }
|
10
|
+
#
|
11
|
+
# This analyzer requires the {FFmpeg}[https://www.ffmpeg.org] system library, which is not provided by Rails.
|
12
|
+
class Analyzer::AudioAnalyzer < Analyzer
|
13
|
+
def self.accept?(blob)
|
14
|
+
blob.audio?
|
15
|
+
end
|
16
|
+
|
17
|
+
def metadata
|
18
|
+
{ duration: duration, bit_rate: bit_rate }.compact
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
def duration
|
23
|
+
duration = audio_stream["duration"]
|
24
|
+
Float(duration) if duration
|
25
|
+
end
|
26
|
+
|
27
|
+
def bit_rate
|
28
|
+
bit_rate = audio_stream["bit_rate"]
|
29
|
+
Integer(bit_rate) if bit_rate
|
30
|
+
end
|
31
|
+
|
32
|
+
def audio_stream
|
33
|
+
@audio_stream ||= streams.detect { |stream| stream["codec_type"] == "audio" } || {}
|
34
|
+
end
|
35
|
+
|
36
|
+
def streams
|
37
|
+
probe["streams"] || []
|
38
|
+
end
|
39
|
+
|
40
|
+
def probe
|
41
|
+
@probe ||= download_blob_to_tempfile { |file| probe_from(file) }
|
42
|
+
end
|
43
|
+
|
44
|
+
def probe_from(file)
|
45
|
+
IO.popen([ffprobe_path,
|
46
|
+
"-print_format", "json",
|
47
|
+
"-show_streams",
|
48
|
+
"-show_format",
|
49
|
+
"-v", "error",
|
50
|
+
file.path
|
51
|
+
]) do |output|
|
52
|
+
JSON.parse(output.read)
|
53
|
+
end
|
54
|
+
rescue Errno::ENOENT
|
55
|
+
logger.info "Skipping audio analysis because FFmpeg isn't installed"
|
56
|
+
{}
|
57
|
+
end
|
58
|
+
|
59
|
+
def ffprobe_path
|
60
|
+
ActiveStorage.paths[:ffprobe] || "ffprobe"
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveAnalysis
|
4
|
+
# This is an abstract base class for image analyzers, which extract width and height from an image blob.
|
5
|
+
#
|
6
|
+
# If the image contains EXIF data indicating its angle is 90 or 270 degrees, its width and height are swapped for convenience.
|
7
|
+
#
|
8
|
+
# Example:
|
9
|
+
#
|
10
|
+
# ActiveAnalysis::CoreExtensionsAnalyzer::ImageAnalyzer::ImageMagick.new(blob).metadata
|
11
|
+
# # => { width: 4104, height: 2736 }
|
12
|
+
class Analyzer::ImageAnalyzer < Analyzer
|
13
|
+
def self.accept?(blob)
|
14
|
+
blob.image?
|
15
|
+
end
|
16
|
+
|
17
|
+
def metadata
|
18
|
+
read_image do |image|
|
19
|
+
if rotated_image?(image)
|
20
|
+
{ width: image.height, height: image.width }
|
21
|
+
else
|
22
|
+
{ width: image.width, height: image.height }
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveAnalysis
|
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
|
37
|
+
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveAnalysis
|
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
|
+
|
34
|
+
def rotated_image?(image)
|
35
|
+
ROTATIONS === image.get("exif-ifd0-Orientation")
|
36
|
+
rescue ::Vips::Error
|
37
|
+
false
|
38
|
+
end
|
39
|
+
|
40
|
+
def valid_image?(image)
|
41
|
+
image.avg
|
42
|
+
true
|
43
|
+
rescue ::Vips::Error
|
44
|
+
false
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
@@ -0,0 +1,81 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveAnalysis
|
4
|
+
# Extracts width, height in pixels and number of pages from a pdf blob.
|
5
|
+
#
|
6
|
+
# Example:
|
7
|
+
#
|
8
|
+
# ActiveAnalysis::CoreExtensionsAnalyzer::PDFAnalyzer::Poppler.new(blob).metadata
|
9
|
+
# # => { width: 4104, height: 2736, pages: 10 }
|
10
|
+
#
|
11
|
+
# This analyzer requires the {poppler}[https://poppler.freedesktop.org/] system library, which is not provided by Rails.
|
12
|
+
class Analyzer::PDFAnalyzer < Analyzer
|
13
|
+
class << self
|
14
|
+
def accept?(blob)
|
15
|
+
blob.content_type == "application/pdf" && pdfinfo_exists?
|
16
|
+
end
|
17
|
+
|
18
|
+
def pdfinfo_path
|
19
|
+
ActiveStorage.paths[:pdfinfo] || "pdfinfo"
|
20
|
+
end
|
21
|
+
|
22
|
+
def pdfinfo_exists?
|
23
|
+
return @pdfinfo_exists if defined?(@pdfinfo_exists)
|
24
|
+
|
25
|
+
@pdfinfo_exists = system(pdfinfo_path, "-v", out: File::NULL, err: File::NULL)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def metadata
|
30
|
+
{ width: width, height: height, pages: pages }.compact
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
def pages
|
35
|
+
pages = info["Pages"]
|
36
|
+
Integer(pages) if pages
|
37
|
+
end
|
38
|
+
|
39
|
+
def width
|
40
|
+
(right - left).floor if cropbox.present?
|
41
|
+
end
|
42
|
+
|
43
|
+
def height
|
44
|
+
(top - bottom).floor if cropbox.present?
|
45
|
+
end
|
46
|
+
|
47
|
+
def left
|
48
|
+
Float cropbox[0]
|
49
|
+
end
|
50
|
+
|
51
|
+
def bottom
|
52
|
+
Float cropbox[1]
|
53
|
+
end
|
54
|
+
|
55
|
+
def right
|
56
|
+
Float cropbox[2]
|
57
|
+
end
|
58
|
+
|
59
|
+
def top
|
60
|
+
Float cropbox[3]
|
61
|
+
end
|
62
|
+
|
63
|
+
def cropbox
|
64
|
+
return @cropbox if defined?(@cropbox)
|
65
|
+
@cropbox = (info["CropBox"] || "").split
|
66
|
+
end
|
67
|
+
|
68
|
+
def info
|
69
|
+
@info ||= download_blob_to_tempfile { |file| info_from(file) }
|
70
|
+
end
|
71
|
+
|
72
|
+
def info_from(file)
|
73
|
+
IO.popen([self.class.pdfinfo_path, "-box", file.path]) do |output|
|
74
|
+
output.read.scan(/^(.*?): *(.*)?/).to_h
|
75
|
+
end
|
76
|
+
rescue Errno::ENOENT
|
77
|
+
logger.info "Skipping pdf analysis due to an error"
|
78
|
+
{}
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,141 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveAnalysis
|
4
|
+
# Extracts the following from a video blob:
|
5
|
+
#
|
6
|
+
# * Width (pixels)
|
7
|
+
# * Height (pixels)
|
8
|
+
# * Duration (seconds)
|
9
|
+
# * Angle (degrees)
|
10
|
+
# * Display aspect ratio
|
11
|
+
# * Audio (true if file has an audio channel, false if not)
|
12
|
+
# * Video (true if file has an video channel, false if not)
|
13
|
+
#
|
14
|
+
# Example:
|
15
|
+
#
|
16
|
+
# ActiveStorage::Analyzer::VideoAnalyzer.new(blob).metadata
|
17
|
+
# # => { width: 640.0, height: 480.0, duration: 5.0, angle: 0, display_aspect_ratio: [4, 3], audio: true, video: true }
|
18
|
+
#
|
19
|
+
# When a video's angle is 90 or 270 degrees, its width and height are automatically swapped for convenience.
|
20
|
+
#
|
21
|
+
# This analyzer requires the {FFmpeg}[https://www.ffmpeg.org] system library, which is not provided by Rails.
|
22
|
+
class Analyzer::VideoAnalyzer < Analyzer
|
23
|
+
def self.accept?(blob)
|
24
|
+
blob.video?
|
25
|
+
end
|
26
|
+
|
27
|
+
def metadata
|
28
|
+
{ width: width, height: height, duration: duration, angle: angle, display_aspect_ratio: display_aspect_ratio, audio: audio?, video: video? }.compact
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
def width
|
33
|
+
if rotated?
|
34
|
+
computed_height || encoded_height
|
35
|
+
else
|
36
|
+
encoded_width
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def height
|
41
|
+
if rotated?
|
42
|
+
encoded_width
|
43
|
+
else
|
44
|
+
computed_height || encoded_height
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def duration
|
49
|
+
duration = video_stream["duration"] || container["duration"]
|
50
|
+
Float(duration) if duration
|
51
|
+
end
|
52
|
+
|
53
|
+
def angle
|
54
|
+
Integer(tags["rotate"]) if tags["rotate"]
|
55
|
+
end
|
56
|
+
|
57
|
+
def display_aspect_ratio
|
58
|
+
if descriptor = video_stream["display_aspect_ratio"]
|
59
|
+
if terms = descriptor.split(":", 2)
|
60
|
+
numerator = Integer(terms[0])
|
61
|
+
denominator = Integer(terms[1])
|
62
|
+
|
63
|
+
[numerator, denominator] unless numerator == 0
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def rotated?
|
69
|
+
angle == 90 || angle == 270
|
70
|
+
end
|
71
|
+
|
72
|
+
def audio?
|
73
|
+
audio_stream.present?
|
74
|
+
end
|
75
|
+
|
76
|
+
def video?
|
77
|
+
video_stream.present? && !video_stream["codec_name"].in?(["jpg", "png"])
|
78
|
+
end
|
79
|
+
|
80
|
+
def computed_height
|
81
|
+
if encoded_width && display_height_scale
|
82
|
+
encoded_width * display_height_scale
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def encoded_width
|
87
|
+
@encoded_width ||= Float(video_stream["width"]) if video_stream["width"]
|
88
|
+
end
|
89
|
+
|
90
|
+
def encoded_height
|
91
|
+
@encoded_height ||= Float(video_stream["height"]) if video_stream["height"]
|
92
|
+
end
|
93
|
+
|
94
|
+
def display_height_scale
|
95
|
+
@display_height_scale ||= Float(display_aspect_ratio.last) / display_aspect_ratio.first if display_aspect_ratio
|
96
|
+
end
|
97
|
+
|
98
|
+
def tags
|
99
|
+
@tags ||= video_stream["tags"] || {}
|
100
|
+
end
|
101
|
+
|
102
|
+
def video_stream
|
103
|
+
@video_stream ||= streams.detect { |stream| stream["codec_type"] == "video" } || {}
|
104
|
+
end
|
105
|
+
|
106
|
+
def audio_stream
|
107
|
+
@audio_stream ||= streams.detect { |stream| stream["codec_type"] == "audio" } || {}
|
108
|
+
end
|
109
|
+
|
110
|
+
def streams
|
111
|
+
probe["streams"] || []
|
112
|
+
end
|
113
|
+
|
114
|
+
def container
|
115
|
+
probe["format"] || {}
|
116
|
+
end
|
117
|
+
|
118
|
+
def probe
|
119
|
+
@probe ||= download_blob_to_tempfile { |file| probe_from(file) }
|
120
|
+
end
|
121
|
+
|
122
|
+
def probe_from(file)
|
123
|
+
IO.popen([ffprobe_path,
|
124
|
+
"-print_format", "json",
|
125
|
+
"-show_streams",
|
126
|
+
"-show_format",
|
127
|
+
"-v", "error",
|
128
|
+
file.path
|
129
|
+
]) do |output|
|
130
|
+
JSON.parse(output.read)
|
131
|
+
end
|
132
|
+
rescue Errno::ENOENT
|
133
|
+
logger.info "Skipping video analysis because FFmpeg isn't installed"
|
134
|
+
{}
|
135
|
+
end
|
136
|
+
|
137
|
+
def ffprobe_path
|
138
|
+
ActiveStorage.paths[:ffprobe] || "ffprobe"
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_storage"
|
4
|
+
|
5
|
+
require "marcel"
|
6
|
+
require "ruby-vips"
|
7
|
+
require "mini_magick"
|
8
|
+
|
9
|
+
require_relative "analyzer"
|
10
|
+
require_relative "analyzer/image_analyzer"
|
11
|
+
require_relative "analyzer/image_analyzer/vips"
|
12
|
+
require_relative "analyzer/image_analyzer/image_magick"
|
13
|
+
require_relative "analyzer/audio_analyzer"
|
14
|
+
require_relative "analyzer/video_analyzer"
|
15
|
+
require_relative "analyzer/pdf_analyzer"
|
16
|
+
|
17
|
+
module ActiveAnalysis
|
18
|
+
class Engine < ::Rails::Engine
|
19
|
+
isolate_namespace ActiveAnalysis
|
20
|
+
|
21
|
+
config.active_analysis = ActiveSupport::OrderedOptions.new
|
22
|
+
config.eager_load_namespaces << ActiveAnalysis
|
23
|
+
|
24
|
+
initializer "active_analysis.configs" do
|
25
|
+
config.after_initialize do |app|
|
26
|
+
ActiveAnalysis.logger = app.config.active_analysis.logger || Rails.logger
|
27
|
+
ActiveAnalysis.analyzers = app.config.active_analysis.analyzers || []
|
28
|
+
ActiveAnalysis.image_library = app.config.active_analysis.image_library || app.config.active_storage.variant_processor
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
initializer "active_analysis.core_extensions" do
|
33
|
+
config.after_initialize do |app|
|
34
|
+
app.config.active_storage.analyzers.delete ActiveStorage::Analyzer::ImageAnalyzer
|
35
|
+
app.config.active_storage.analyzers.delete ActiveStorage::Analyzer::VideoAnalyzer
|
36
|
+
|
37
|
+
app.config.active_storage.analyzers.append Analyzer::ImageAnalyzer::Vips
|
38
|
+
app.config.active_storage.analyzers.append Analyzer::ImageAnalyzer::ImageMagick
|
39
|
+
app.config.active_storage.analyzers.append Analyzer::VideoAnalyzer
|
40
|
+
app.config.active_storage.analyzers.append Analyzer::AudioAnalyzer
|
41
|
+
app.config.active_storage.analyzers.append Analyzer::PDFAnalyzer
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveAnalysis
|
4
|
+
# Returns the version of the currently loaded Active Storage as a <tt>Gem::Version</tt>.
|
5
|
+
def self.gem_version
|
6
|
+
Gem::Version.new VERSION::STRING
|
7
|
+
end
|
8
|
+
|
9
|
+
module VERSION
|
10
|
+
MAJOR = 0
|
11
|
+
MINOR = 1
|
12
|
+
TINY = 2
|
13
|
+
PRE = nil
|
14
|
+
|
15
|
+
STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
|
16
|
+
end
|
17
|
+
end
|
metadata
ADDED
@@ -0,0 +1,226 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: active_analysis
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.2
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Breno Gazzola
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2021-06-11 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: activestorage
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '6.1'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '6.1'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: activesupport
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '6.1'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '6.1'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: mini_magick
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '4.1'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '4.1'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: ruby-vips
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '2.0'
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '2.0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: platform_agent
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '1.0'
|
76
|
+
type: :runtime
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '1.0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: sqlite3
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '1.4'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '1.4'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: byebug
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - "~>"
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '11.1'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - "~>"
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '11.1'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: rubocop
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - "~>"
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '1.14'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - "~>"
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '1.14'
|
125
|
+
- !ruby/object:Gem::Dependency
|
126
|
+
name: rubocop-performance
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - "~>"
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: '1.11'
|
132
|
+
type: :development
|
133
|
+
prerelease: false
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - "~>"
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: '1.11'
|
139
|
+
- !ruby/object:Gem::Dependency
|
140
|
+
name: rubocop-packaging
|
141
|
+
requirement: !ruby/object:Gem::Requirement
|
142
|
+
requirements:
|
143
|
+
- - "~>"
|
144
|
+
- !ruby/object:Gem::Version
|
145
|
+
version: '0.5'
|
146
|
+
type: :development
|
147
|
+
prerelease: false
|
148
|
+
version_requirements: !ruby/object:Gem::Requirement
|
149
|
+
requirements:
|
150
|
+
- - "~>"
|
151
|
+
- !ruby/object:Gem::Version
|
152
|
+
version: '0.5'
|
153
|
+
- !ruby/object:Gem::Dependency
|
154
|
+
name: rubocop-rails
|
155
|
+
requirement: !ruby/object:Gem::Requirement
|
156
|
+
requirements:
|
157
|
+
- - "~>"
|
158
|
+
- !ruby/object:Gem::Version
|
159
|
+
version: '2.10'
|
160
|
+
type: :development
|
161
|
+
prerelease: false
|
162
|
+
version_requirements: !ruby/object:Gem::Requirement
|
163
|
+
requirements:
|
164
|
+
- - "~>"
|
165
|
+
- !ruby/object:Gem::Version
|
166
|
+
version: '2.10'
|
167
|
+
description:
|
168
|
+
email:
|
169
|
+
- breno@festalab.com
|
170
|
+
executables: []
|
171
|
+
extensions: []
|
172
|
+
extra_rdoc_files: []
|
173
|
+
files:
|
174
|
+
- CHANGELOG.md
|
175
|
+
- MIT-LICENSE
|
176
|
+
- README.md
|
177
|
+
- Rakefile
|
178
|
+
- app/assets/config/active_analysis_manifest.js
|
179
|
+
- app/assets/stylesheets/active_analysis/application.css
|
180
|
+
- app/controllers/active_analysis/application_controller.rb
|
181
|
+
- app/helpers/active_analysis/application_helper.rb
|
182
|
+
- app/jobs/active_analysis/application_job.rb
|
183
|
+
- app/mailers/active_analysis/application_mailer.rb
|
184
|
+
- app/models/active_analysis/application_record.rb
|
185
|
+
- app/views/layouts/active_analysis/application.html.erb
|
186
|
+
- config/routes.rb
|
187
|
+
- lib/active_analysis.rb
|
188
|
+
- lib/active_analysis/analyzer.rb
|
189
|
+
- lib/active_analysis/analyzer/audio_analyzer.rb
|
190
|
+
- lib/active_analysis/analyzer/image_analyzer.rb
|
191
|
+
- lib/active_analysis/analyzer/image_analyzer/image_magick.rb
|
192
|
+
- lib/active_analysis/analyzer/image_analyzer/vips.rb
|
193
|
+
- lib/active_analysis/analyzer/pdf_analyzer.rb
|
194
|
+
- lib/active_analysis/analyzer/video_analyzer.rb
|
195
|
+
- lib/active_analysis/engine.rb
|
196
|
+
- lib/active_analysis/gem_version.rb
|
197
|
+
- lib/active_analysis/version.rb
|
198
|
+
- lib/tasks/active_analysis_tasks.rake
|
199
|
+
homepage: https://github.com/FestaLab/active_analysis
|
200
|
+
licenses:
|
201
|
+
- MIT
|
202
|
+
metadata:
|
203
|
+
allowed_push_host: https://rubygems.org
|
204
|
+
homepage_uri: https://github.com/FestaLab/active_analysis
|
205
|
+
source_code_uri: https://github.com/FestaLab/active_analysis
|
206
|
+
changelog_uri: https://github.com/FestaLab/active_analysis/CHANGELOG.mg
|
207
|
+
post_install_message:
|
208
|
+
rdoc_options: []
|
209
|
+
require_paths:
|
210
|
+
- lib
|
211
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
212
|
+
requirements:
|
213
|
+
- - ">="
|
214
|
+
- !ruby/object:Gem::Version
|
215
|
+
version: 2.5.0
|
216
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
217
|
+
requirements:
|
218
|
+
- - ">="
|
219
|
+
- !ruby/object:Gem::Version
|
220
|
+
version: '0'
|
221
|
+
requirements: []
|
222
|
+
rubygems_version: 3.0.3
|
223
|
+
signing_key:
|
224
|
+
specification_version: 4
|
225
|
+
summary: Collection of Active Storage analyzers
|
226
|
+
test_files: []
|