activestorage 5.2.0.beta2 → 5.2.0.rc1
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.
- checksums.yaml +5 -5
- data/CHANGELOG.md +25 -0
- data/MIT-LICENSE +1 -1
- data/README.md +15 -1
- data/app/assets/javascripts/activestorage.js +1 -1
- data/app/controllers/active_storage/blobs_controller.rb +4 -6
- data/app/controllers/active_storage/previews_controller.rb +4 -6
- data/app/controllers/active_storage/variants_controller.rb +4 -6
- data/app/controllers/concerns/active_storage/set_blob.rb +16 -0
- data/app/javascript/activestorage/blob_record.js +17 -3
- data/app/javascript/activestorage/helpers.js +10 -1
- data/app/models/active_storage/attachment.rb +5 -1
- data/app/models/active_storage/blob.rb +15 -134
- data/app/models/active_storage/blob/analyzable.rb +57 -0
- data/app/models/active_storage/blob/identifiable.rb +11 -0
- data/app/models/active_storage/blob/representable.rb +93 -0
- data/app/models/active_storage/filename.rb +1 -1
- data/app/models/active_storage/filename/parameters.rb +1 -1
- data/app/models/active_storage/identification.rb +38 -0
- data/app/models/active_storage/variant.rb +51 -5
- data/app/models/active_storage/variation.rb +19 -5
- data/config/routes.rb +7 -7
- data/lib/active_storage.rb +8 -1
- data/lib/active_storage/analyzer.rb +1 -1
- data/lib/active_storage/analyzer/image_analyzer.rb +1 -2
- data/lib/active_storage/analyzer/video_analyzer.rb +51 -10
- data/lib/active_storage/attached/macros.rb +4 -4
- data/lib/active_storage/downloading.rb +16 -4
- data/lib/active_storage/engine.rb +18 -1
- data/lib/active_storage/errors.rb +7 -0
- data/lib/active_storage/gem_version.rb +1 -1
- data/lib/active_storage/log_subscriber.rb +4 -0
- data/lib/active_storage/previewer.rb +21 -5
- data/lib/active_storage/previewer/pdf_previewer.rb +10 -1
- data/lib/active_storage/previewer/video_previewer.rb +5 -1
- data/lib/active_storage/service.rb +8 -3
- data/lib/active_storage/service/azure_storage_service.rb +24 -8
- data/lib/active_storage/service/disk_service.rb +26 -24
- data/lib/active_storage/service/gcs_service.rb +21 -7
- data/lib/active_storage/service/mirror_service.rb +5 -0
- data/lib/active_storage/service/s3_service.rb +13 -7
- data/lib/tasks/activestorage.rake +5 -1
- metadata +43 -9
@@ -0,0 +1,57 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_storage/analyzer/null_analyzer"
|
4
|
+
|
5
|
+
module ActiveStorage::Blob::Analyzable
|
6
|
+
# Extracts and stores metadata from the file associated with this blob using a relevant analyzer. Active Storage comes
|
7
|
+
# with built-in analyzers for images and videos. See ActiveStorage::Analyzer::ImageAnalyzer and
|
8
|
+
# ActiveStorage::Analyzer::VideoAnalyzer for information about the specific attributes they extract and the third-party
|
9
|
+
# libraries they require.
|
10
|
+
#
|
11
|
+
# To choose the analyzer for a blob, Active Storage calls +accept?+ on each registered analyzer in order. It uses the
|
12
|
+
# first analyzer for which +accept?+ returns true when given the blob. If no registered analyzer accepts the blob, no
|
13
|
+
# metadata is extracted from it.
|
14
|
+
#
|
15
|
+
# In a Rails application, add or remove analyzers by manipulating +Rails.application.config.active_storage.analyzers+
|
16
|
+
# in an initializer:
|
17
|
+
#
|
18
|
+
# # Add a custom analyzer for Microsoft Office documents:
|
19
|
+
# Rails.application.config.active_storage.analyzers.append DOCXAnalyzer
|
20
|
+
#
|
21
|
+
# # Remove the built-in video analyzer:
|
22
|
+
# Rails.application.config.active_storage.analyzers.delete ActiveStorage::Analyzer::VideoAnalyzer
|
23
|
+
#
|
24
|
+
# Outside of a Rails application, manipulate +ActiveStorage.analyzers+ instead.
|
25
|
+
#
|
26
|
+
# You won't ordinarily need to call this method from a Rails application. New blobs are automatically and asynchronously
|
27
|
+
# analyzed via #analyze_later when they're attached for the first time.
|
28
|
+
def analyze
|
29
|
+
update! metadata: metadata.merge(extract_metadata_via_analyzer)
|
30
|
+
end
|
31
|
+
|
32
|
+
# Enqueues an ActiveStorage::AnalyzeJob which calls #analyze.
|
33
|
+
#
|
34
|
+
# This method is automatically called for a blob when it's attached for the first time. You can call it to analyze a blob
|
35
|
+
# again (e.g. if you add a new analyzer or modify an existing one).
|
36
|
+
def analyze_later
|
37
|
+
ActiveStorage::AnalyzeJob.perform_later(self)
|
38
|
+
end
|
39
|
+
|
40
|
+
# Returns true if the blob has been analyzed.
|
41
|
+
def analyzed?
|
42
|
+
analyzed
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
def extract_metadata_via_analyzer
|
47
|
+
analyzer.metadata.merge(analyzed: true)
|
48
|
+
end
|
49
|
+
|
50
|
+
def analyzer
|
51
|
+
analyzer_class.new(self)
|
52
|
+
end
|
53
|
+
|
54
|
+
def analyzer_class
|
55
|
+
ActiveStorage.analyzers.detect { |klass| klass.accept?(self) } || ActiveStorage::Analyzer::NullAnalyzer
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveStorage::Blob::Representable
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
included do
|
7
|
+
has_one_attached :preview_image
|
8
|
+
end
|
9
|
+
|
10
|
+
# Returns an ActiveStorage::Variant instance with the set of +transformations+ provided. This is only relevant for image
|
11
|
+
# files, and it allows any image to be transformed for size, colors, and the like. Example:
|
12
|
+
#
|
13
|
+
# avatar.variant(resize: "100x100").processed.service_url
|
14
|
+
#
|
15
|
+
# This will create and process a variant of the avatar blob that's constrained to a height and width of 100px.
|
16
|
+
# Then it'll upload said variant to the service according to a derivative key of the blob and the transformations.
|
17
|
+
#
|
18
|
+
# Frequently, though, you don't actually want to transform the variant right away. But rather simply refer to a
|
19
|
+
# specific variant that can be created by a controller on-demand. Like so:
|
20
|
+
#
|
21
|
+
# <%= image_tag Current.user.avatar.variant(resize: "100x100") %>
|
22
|
+
#
|
23
|
+
# This will create a URL for that specific blob with that specific variant, which the ActiveStorage::VariantsController
|
24
|
+
# can then produce on-demand.
|
25
|
+
#
|
26
|
+
# Raises ActiveStorage::InvariableError if ImageMagick cannot transform the blob. To determine whether a blob is
|
27
|
+
# variable, call ActiveStorage::Blob#variable?.
|
28
|
+
def variant(transformations)
|
29
|
+
if variable?
|
30
|
+
ActiveStorage::Variant.new(self, ActiveStorage::Variation.wrap(transformations))
|
31
|
+
else
|
32
|
+
raise ActiveStorage::InvariableError
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# Returns true if ImageMagick can transform the blob (its content type is in +ActiveStorage.variable_content_types+).
|
37
|
+
def variable?
|
38
|
+
ActiveStorage.variable_content_types.include?(content_type)
|
39
|
+
end
|
40
|
+
|
41
|
+
|
42
|
+
# Returns an ActiveStorage::Preview instance with the set of +transformations+ provided. A preview is an image generated
|
43
|
+
# from a non-image blob. Active Storage comes with built-in previewers for videos and PDF documents. The video previewer
|
44
|
+
# extracts the first frame from a video and the PDF previewer extracts the first page from a PDF document.
|
45
|
+
#
|
46
|
+
# blob.preview(resize: "100x100").processed.service_url
|
47
|
+
#
|
48
|
+
# Avoid processing previews synchronously in views. Instead, link to a controller action that processes them on demand.
|
49
|
+
# Active Storage provides one, but you may want to create your own (for example, if you need authentication). Here’s
|
50
|
+
# how to use the built-in version:
|
51
|
+
#
|
52
|
+
# <%= image_tag video.preview(resize: "100x100") %>
|
53
|
+
#
|
54
|
+
# This method raises ActiveStorage::UnpreviewableError if no previewer accepts the receiving blob. To determine
|
55
|
+
# whether a blob is accepted by any previewer, call ActiveStorage::Blob#previewable?.
|
56
|
+
def preview(transformations)
|
57
|
+
if previewable?
|
58
|
+
ActiveStorage::Preview.new(self, ActiveStorage::Variation.wrap(transformations))
|
59
|
+
else
|
60
|
+
raise ActiveStorage::UnpreviewableError
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
# Returns true if any registered previewer accepts the blob. By default, this will return true for videos and PDF documents.
|
65
|
+
def previewable?
|
66
|
+
ActiveStorage.previewers.any? { |klass| klass.accept?(self) }
|
67
|
+
end
|
68
|
+
|
69
|
+
|
70
|
+
# Returns an ActiveStorage::Preview for a previewable blob or an ActiveStorage::Variant for a variable image blob.
|
71
|
+
#
|
72
|
+
# blob.representation(resize: "100x100").processed.service_url
|
73
|
+
#
|
74
|
+
# Raises ActiveStorage::UnrepresentableError if the receiving blob is neither variable nor previewable. Call
|
75
|
+
# ActiveStorage::Blob#representable? to determine whether a blob is representable.
|
76
|
+
#
|
77
|
+
# See ActiveStorage::Blob#preview and ActiveStorage::Blob#variant for more information.
|
78
|
+
def representation(transformations)
|
79
|
+
case
|
80
|
+
when previewable?
|
81
|
+
preview transformations
|
82
|
+
when variable?
|
83
|
+
variant transformations
|
84
|
+
else
|
85
|
+
raise ActiveStorage::UnrepresentableError
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
# Returns true if the blob is variable or previewable.
|
90
|
+
def representable?
|
91
|
+
variable? || previewable?
|
92
|
+
end
|
93
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class ActiveStorage::Identification
|
4
|
+
attr_reader :blob
|
5
|
+
|
6
|
+
def initialize(blob)
|
7
|
+
@blob = blob
|
8
|
+
end
|
9
|
+
|
10
|
+
def apply
|
11
|
+
blob.update!(content_type: content_type, identified: true) unless blob.identified?
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
def content_type
|
16
|
+
Marcel::MimeType.for(identifiable_chunk, name: filename, declared_type: declared_content_type)
|
17
|
+
end
|
18
|
+
|
19
|
+
|
20
|
+
def identifiable_chunk
|
21
|
+
Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https") do |client|
|
22
|
+
client.get(uri, "Range" => "0-4096").body
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def uri
|
27
|
+
@uri ||= URI.parse(blob.service_url)
|
28
|
+
end
|
29
|
+
|
30
|
+
|
31
|
+
def filename
|
32
|
+
blob.filename.to_s
|
33
|
+
end
|
34
|
+
|
35
|
+
def declared_content_type
|
36
|
+
blob.content_type
|
37
|
+
end
|
38
|
+
end
|
@@ -1,5 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "active_storage/downloading"
|
4
|
+
|
3
5
|
# Image blobs can have variants that are the result of a set of transformations applied to the original.
|
4
6
|
# These variants are used to create thumbnails, fixed-size avatars, or any other derivative image from the
|
5
7
|
# original.
|
@@ -16,7 +18,7 @@
|
|
16
18
|
# To refer to such a delayed on-demand variant, simply link to the variant through the resolved route provided
|
17
19
|
# by Active Storage like so:
|
18
20
|
#
|
19
|
-
# <%= image_tag
|
21
|
+
# <%= image_tag Current.user.avatar.variant(resize: "100x100") %>
|
20
22
|
#
|
21
23
|
# This will create a URL for that specific blob with that specific variant, which the ActiveStorage::VariantsController
|
22
24
|
# can then produce on-demand.
|
@@ -35,6 +37,10 @@
|
|
35
37
|
#
|
36
38
|
# avatar.variant(resize: "100x100", monochrome: true, flip: "-90")
|
37
39
|
class ActiveStorage::Variant
|
40
|
+
include ActiveStorage::Downloading
|
41
|
+
|
42
|
+
WEB_IMAGE_CONTENT_TYPES = %w( image/png image/jpeg image/jpg image/gif )
|
43
|
+
|
38
44
|
attr_reader :blob, :variation
|
39
45
|
delegate :service, to: :blob
|
40
46
|
|
@@ -62,7 +68,7 @@ class ActiveStorage::Variant
|
|
62
68
|
# for a variant that points to the ActiveStorage::VariantsController, which in turn will use this +service_call+ method
|
63
69
|
# for its redirection.
|
64
70
|
def service_url(expires_in: service.url_expires_in, disposition: :inline)
|
65
|
-
service.url key, expires_in: expires_in, disposition: disposition, filename:
|
71
|
+
service.url key, expires_in: expires_in, disposition: disposition, filename: filename, content_type: content_type
|
66
72
|
end
|
67
73
|
|
68
74
|
# Returns the receiving variant. Allows ActiveStorage::Variant and ActiveStorage::Preview instances to be used interchangeably.
|
@@ -76,11 +82,51 @@ class ActiveStorage::Variant
|
|
76
82
|
end
|
77
83
|
|
78
84
|
def process
|
79
|
-
|
85
|
+
open_image do |image|
|
86
|
+
transform image
|
87
|
+
format image
|
88
|
+
upload image
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
|
93
|
+
def filename
|
94
|
+
if WEB_IMAGE_CONTENT_TYPES.include?(blob.content_type)
|
95
|
+
blob.filename
|
96
|
+
else
|
97
|
+
ActiveStorage::Filename.new("#{blob.filename.base}.png")
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
def content_type
|
102
|
+
blob.content_type.presence_in(WEB_IMAGE_CONTENT_TYPES) || "image/png"
|
103
|
+
end
|
104
|
+
|
105
|
+
|
106
|
+
def open_image(&block)
|
107
|
+
image = download_image
|
108
|
+
|
109
|
+
begin
|
110
|
+
yield image
|
111
|
+
ensure
|
112
|
+
image.destroy!
|
113
|
+
end
|
80
114
|
end
|
81
115
|
|
82
|
-
def
|
116
|
+
def download_image
|
83
117
|
require "mini_magick"
|
84
|
-
|
118
|
+
MiniMagick::Image.create { |file| download_blob_to(file) }
|
119
|
+
end
|
120
|
+
|
121
|
+
def transform(image)
|
122
|
+
variation.transform(image)
|
123
|
+
end
|
124
|
+
|
125
|
+
def format(image)
|
126
|
+
image.format("PNG") unless WEB_IMAGE_CONTENT_TYPES.include?(blob.content_type)
|
127
|
+
end
|
128
|
+
|
129
|
+
def upload(image)
|
130
|
+
File.open(image.path, "r") { |file| service.upload(key, file) }
|
85
131
|
end
|
86
132
|
end
|
@@ -46,11 +46,17 @@ class ActiveStorage::Variation
|
|
46
46
|
# Accepts an open MiniMagick image instance, like what's returned by <tt>MiniMagick::Image.read(io)</tt>,
|
47
47
|
# and performs the +transformations+ against it. The transformed image instance is then returned.
|
48
48
|
def transform(image)
|
49
|
-
|
50
|
-
|
51
|
-
image.
|
52
|
-
|
53
|
-
|
49
|
+
ActiveSupport::Notifications.instrument("transform.active_storage") do
|
50
|
+
transformations.each do |name, argument_or_subtransformations|
|
51
|
+
image.mogrify do |command|
|
52
|
+
if name.to_s == "combine_options"
|
53
|
+
argument_or_subtransformations.each do |subtransformation_name, subtransformation_argument|
|
54
|
+
pass_transform_argument(command, subtransformation_name, subtransformation_argument)
|
55
|
+
end
|
56
|
+
else
|
57
|
+
pass_transform_argument(command, name, argument_or_subtransformations)
|
58
|
+
end
|
59
|
+
end
|
54
60
|
end
|
55
61
|
end
|
56
62
|
end
|
@@ -61,6 +67,14 @@ class ActiveStorage::Variation
|
|
61
67
|
end
|
62
68
|
|
63
69
|
private
|
70
|
+
def pass_transform_argument(command, method, argument)
|
71
|
+
if eligible_argument?(argument)
|
72
|
+
command.public_send(method, argument)
|
73
|
+
else
|
74
|
+
command.public_send(method)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
64
78
|
def eligible_argument?(argument)
|
65
79
|
argument.present? && argument != true
|
66
80
|
end
|
data/config/routes.rb
CHANGED
@@ -1,17 +1,17 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
Rails.application.routes.draw do
|
4
|
-
get "/rails/active_storage/blobs/:signed_id/*filename" => "active_storage/blobs#show", as: :rails_service_blob
|
4
|
+
get "/rails/active_storage/blobs/:signed_id/*filename" => "active_storage/blobs#show", as: :rails_service_blob
|
5
5
|
|
6
6
|
direct :rails_blob do |blob, options|
|
7
7
|
route_for(:rails_service_blob, blob.signed_id, blob.filename, options)
|
8
8
|
end
|
9
9
|
|
10
|
-
resolve("ActiveStorage::Blob") { |blob, options| route_for(:rails_blob, blob) }
|
10
|
+
resolve("ActiveStorage::Blob") { |blob, options| route_for(:rails_blob, blob, options) }
|
11
11
|
resolve("ActiveStorage::Attachment") { |attachment, options| route_for(:rails_blob, attachment.blob, options) }
|
12
12
|
|
13
13
|
|
14
|
-
get "/rails/active_storage/variants/:signed_blob_id/:variation_key/*filename" => "active_storage/variants#show", as: :rails_blob_variation
|
14
|
+
get "/rails/active_storage/variants/:signed_blob_id/:variation_key/*filename" => "active_storage/variants#show", as: :rails_blob_variation
|
15
15
|
|
16
16
|
direct :rails_variant do |variant, options|
|
17
17
|
signed_blob_id = variant.blob.signed_id
|
@@ -24,7 +24,7 @@ Rails.application.routes.draw do
|
|
24
24
|
resolve("ActiveStorage::Variant") { |variant, options| route_for(:rails_variant, variant, options) }
|
25
25
|
|
26
26
|
|
27
|
-
get "/rails/active_storage/previews/:signed_blob_id/:variation_key/*filename" => "active_storage/previews#show", as: :rails_blob_preview
|
27
|
+
get "/rails/active_storage/previews/:signed_blob_id/:variation_key/*filename" => "active_storage/previews#show", as: :rails_blob_preview
|
28
28
|
|
29
29
|
direct :rails_preview do |preview, options|
|
30
30
|
signed_blob_id = preview.blob.signed_id
|
@@ -37,7 +37,7 @@ Rails.application.routes.draw do
|
|
37
37
|
resolve("ActiveStorage::Preview") { |preview, options| route_for(:rails_preview, preview, options) }
|
38
38
|
|
39
39
|
|
40
|
-
get "/rails/active_storage/disk/:encoded_key/*filename" => "active_storage/disk#show", as: :rails_disk_service
|
41
|
-
put "/rails/active_storage/disk/:encoded_token" => "active_storage/disk#update", as: :update_rails_disk_service
|
42
|
-
post "/rails/active_storage/direct_uploads" => "active_storage/direct_uploads#create", as: :rails_direct_uploads
|
40
|
+
get "/rails/active_storage/disk/:encoded_key/*filename" => "active_storage/disk#show", as: :rails_disk_service
|
41
|
+
put "/rails/active_storage/disk/:encoded_token" => "active_storage/disk#update", as: :update_rails_disk_service
|
42
|
+
post "/rails/active_storage/direct_uploads" => "active_storage/direct_uploads#create", as: :rails_direct_uploads
|
43
43
|
end
|
data/lib/active_storage.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
#--
|
4
|
-
# Copyright (c) 2017 David Heinemeier Hansson
|
4
|
+
# Copyright (c) 2017-2018 David Heinemeier Hansson, Basecamp
|
5
5
|
#
|
6
6
|
# Permission is hereby granted, free of charge, to any person obtaining
|
7
7
|
# a copy of this software and associated documentation files (the
|
@@ -26,7 +26,11 @@
|
|
26
26
|
require "active_record"
|
27
27
|
require "active_support"
|
28
28
|
require "active_support/rails"
|
29
|
+
|
29
30
|
require "active_storage/version"
|
31
|
+
require "active_storage/errors"
|
32
|
+
|
33
|
+
require "marcel"
|
30
34
|
|
31
35
|
module ActiveStorage
|
32
36
|
extend ActiveSupport::Autoload
|
@@ -41,4 +45,7 @@ module ActiveStorage
|
|
41
45
|
mattr_accessor :queue
|
42
46
|
mattr_accessor :previewers, default: []
|
43
47
|
mattr_accessor :analyzers, default: []
|
48
|
+
mattr_accessor :paths, default: {}
|
49
|
+
mattr_accessor :variable_content_types, default: []
|
50
|
+
mattr_accessor :content_types_to_serve_as_binary, default: []
|
44
51
|
end
|
@@ -9,8 +9,7 @@ module ActiveStorage
|
|
9
9
|
# # => { width: 4104, height: 2736 }
|
10
10
|
#
|
11
11
|
# This analyzer relies on the third-party {MiniMagick}[https://github.com/minimagick/minimagick] gem. MiniMagick requires
|
12
|
-
# the {ImageMagick}[http://www.imagemagick.org] system library.
|
13
|
-
# install them yourself to use this analyzer.
|
12
|
+
# the {ImageMagick}[http://www.imagemagick.org] system library.
|
14
13
|
class Analyzer::ImageAnalyzer < Analyzer
|
15
14
|
def self.accept?(blob)
|
16
15
|
blob.image?
|
@@ -9,31 +9,40 @@ module ActiveStorage
|
|
9
9
|
# * Height (pixels)
|
10
10
|
# * Duration (seconds)
|
11
11
|
# * Angle (degrees)
|
12
|
-
# *
|
12
|
+
# * Display aspect ratio
|
13
13
|
#
|
14
14
|
# Example:
|
15
15
|
#
|
16
16
|
# ActiveStorage::VideoAnalyzer.new(blob).metadata
|
17
|
-
# # => { width: 640, height: 480, duration: 5.0, angle: 0,
|
17
|
+
# # => { width: 640.0, height: 480.0, duration: 5.0, angle: 0, display_aspect_ratio: [4, 3] }
|
18
18
|
#
|
19
|
-
#
|
20
|
-
#
|
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.
|
21
22
|
class Analyzer::VideoAnalyzer < Analyzer
|
22
23
|
def self.accept?(blob)
|
23
24
|
blob.video?
|
24
25
|
end
|
25
26
|
|
26
27
|
def metadata
|
27
|
-
{ width: width, height: height, duration: duration, angle: angle,
|
28
|
+
{ width: width, height: height, duration: duration, angle: angle, display_aspect_ratio: display_aspect_ratio }.compact
|
28
29
|
end
|
29
30
|
|
30
31
|
private
|
31
32
|
def width
|
32
|
-
|
33
|
+
if rotated?
|
34
|
+
computed_height || encoded_height
|
35
|
+
else
|
36
|
+
encoded_width
|
37
|
+
end
|
33
38
|
end
|
34
39
|
|
35
40
|
def height
|
36
|
-
|
41
|
+
if rotated?
|
42
|
+
encoded_width
|
43
|
+
else
|
44
|
+
computed_height || encoded_height
|
45
|
+
end
|
37
46
|
end
|
38
47
|
|
39
48
|
def duration
|
@@ -44,12 +53,40 @@ module ActiveStorage
|
|
44
53
|
Integer(tags["rotate"]) if tags["rotate"]
|
45
54
|
end
|
46
55
|
|
47
|
-
def
|
56
|
+
def display_aspect_ratio
|
48
57
|
if descriptor = video_stream["display_aspect_ratio"]
|
49
|
-
descriptor.split(":", 2)
|
58
|
+
if terms = descriptor.split(":", 2)
|
59
|
+
numerator = Integer(terms[0])
|
60
|
+
denominator = Integer(terms[1])
|
61
|
+
|
62
|
+
[numerator, denominator] unless numerator == 0
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
|
68
|
+
def rotated?
|
69
|
+
angle == 90 || angle == 270
|
70
|
+
end
|
71
|
+
|
72
|
+
def computed_height
|
73
|
+
if encoded_width && display_height_scale
|
74
|
+
encoded_width * display_height_scale
|
50
75
|
end
|
51
76
|
end
|
52
77
|
|
78
|
+
def encoded_width
|
79
|
+
@encoded_width ||= Float(video_stream["width"]) if video_stream["width"]
|
80
|
+
end
|
81
|
+
|
82
|
+
def encoded_height
|
83
|
+
@encoded_height ||= Float(video_stream["height"]) if video_stream["height"]
|
84
|
+
end
|
85
|
+
|
86
|
+
def display_height_scale
|
87
|
+
@display_height_scale ||= Float(display_aspect_ratio.last) / display_aspect_ratio.first if display_aspect_ratio
|
88
|
+
end
|
89
|
+
|
53
90
|
|
54
91
|
def tags
|
55
92
|
@tags ||= video_stream["tags"] || {}
|
@@ -68,12 +105,16 @@ module ActiveStorage
|
|
68
105
|
end
|
69
106
|
|
70
107
|
def probe_from(file)
|
71
|
-
IO.popen([
|
108
|
+
IO.popen([ ffprobe_path, "-print_format", "json", "-show_streams", "-v", "error", file.path ]) do |output|
|
72
109
|
JSON.parse(output.read)
|
73
110
|
end
|
74
111
|
rescue Errno::ENOENT
|
75
112
|
logger.info "Skipping video analysis because ffmpeg isn't installed"
|
76
113
|
{}
|
77
114
|
end
|
115
|
+
|
116
|
+
def ffprobe_path
|
117
|
+
ActiveStorage.paths[:ffprobe] || "ffprobe"
|
118
|
+
end
|
78
119
|
end
|
79
120
|
end
|