aboutsummaryrefslogtreecommitdiffstats
path: root/activestorage/lib
diff options
context:
space:
mode:
authorGeorge Claghorn <george.claghorn@gmail.com>2017-10-22 13:16:59 -0400
committerGitHub <noreply@github.com>2017-10-22 13:16:59 -0400
commit605484079d297d1ba6835628465be81f03c052ee (patch)
treee4e32f5e47f4bf48cb9eb9fc8d80c7da268ae262 /activestorage/lib
parent6525e7fb2e534a6564438008f9c02cf29eb37483 (diff)
downloadrails-605484079d297d1ba6835628465be81f03c052ee.tar.gz
rails-605484079d297d1ba6835628465be81f03c052ee.tar.bz2
rails-605484079d297d1ba6835628465be81f03c052ee.zip
Extract metadata from images and videos
Diffstat (limited to 'activestorage/lib')
-rw-r--r--activestorage/lib/active_storage.rb3
-rw-r--r--activestorage/lib/active_storage/analyzer.rb33
-rw-r--r--activestorage/lib/active_storage/analyzer/image_analyzer.rb36
-rw-r--r--activestorage/lib/active_storage/analyzer/null_analyzer.rb13
-rw-r--r--activestorage/lib/active_storage/analyzer/video_analyzer.rb79
-rw-r--r--activestorage/lib/active_storage/attached/one.rb6
-rw-r--r--activestorage/lib/active_storage/downloading.rb21
-rw-r--r--activestorage/lib/active_storage/engine.rb14
-rw-r--r--activestorage/lib/active_storage/log_subscriber.rb2
-rw-r--r--activestorage/lib/active_storage/previewer.rb27
-rw-r--r--activestorage/lib/active_storage/previewer/pdf_previewer.rb2
-rw-r--r--activestorage/lib/active_storage/previewer/video_previewer.rb10
-rw-r--r--activestorage/lib/active_storage/service.rb2
13 files changed, 218 insertions, 30 deletions
diff --git a/activestorage/lib/active_storage.rb b/activestorage/lib/active_storage.rb
index 9ac1e7ab54..cfdb2a8acd 100644
--- a/activestorage/lib/active_storage.rb
+++ b/activestorage/lib/active_storage.rb
@@ -34,7 +34,10 @@ module ActiveStorage
autoload :Attached
autoload :Service
autoload :Previewer
+ autoload :Analyzer
+ mattr_accessor :logger
mattr_accessor :verifier
mattr_accessor :previewers, default: []
+ mattr_accessor :analyzers, default: []
end
diff --git a/activestorage/lib/active_storage/analyzer.rb b/activestorage/lib/active_storage/analyzer.rb
new file mode 100644
index 0000000000..837785a12b
--- /dev/null
+++ b/activestorage/lib/active_storage/analyzer.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+require "active_storage/downloading"
+
+module ActiveStorage
+ # This is an abstract base class for analyzers, which extract metadata from blobs. See
+ # ActiveStorage::Analyzer::ImageAnalyzer for an example of a concrete subclass.
+ class Analyzer
+ include Downloading
+
+ attr_reader :blob
+
+ # Implement this method in a concrete subclass. Have it return true when given a blob from which
+ # the analyzer can extract metadata.
+ def self.accept?(blob)
+ false
+ end
+
+ def initialize(blob)
+ @blob = blob
+ end
+
+ # Override this method in a concrete subclass. Have it return a Hash of metadata.
+ def metadata
+ raise NotImplementedError
+ end
+
+ private
+ def logger
+ ActiveStorage.logger
+ end
+ end
+end
diff --git a/activestorage/lib/active_storage/analyzer/image_analyzer.rb b/activestorage/lib/active_storage/analyzer/image_analyzer.rb
new file mode 100644
index 0000000000..b25d2092b9
--- /dev/null
+++ b/activestorage/lib/active_storage/analyzer/image_analyzer.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+module ActiveStorage
+ # Extracts width and height in pixels from an image blob.
+ #
+ # Example:
+ #
+ # ActiveStorage::Analyzer::ImageAnalyzer.new(blob).metadata
+ # # => { width: 4104, height: 2736 }
+ #
+ # This analyzer relies on the third-party [MiniMagick]{https://github.com/minimagick/minimagick} gem. MiniMagick requires
+ # the [ImageMagick]{http://www.imagemagick.org} system library. These libraries are not provided by Rails; you must
+ # install them yourself to use this analyzer.
+ class Analyzer::ImageAnalyzer < Analyzer
+ def self.accept?(blob)
+ blob.image?
+ end
+
+ def metadata
+ read_image do |image|
+ { width: image.width, height: image.height }
+ end
+ rescue LoadError
+ logger.info "Skipping image analysis because the mini_magick gem isn't installed"
+ {}
+ end
+
+ private
+ def read_image
+ download_blob_to_tempfile do |file|
+ require "mini_magick"
+ yield MiniMagick::Image.new(file.path)
+ end
+ end
+ end
+end
diff --git a/activestorage/lib/active_storage/analyzer/null_analyzer.rb b/activestorage/lib/active_storage/analyzer/null_analyzer.rb
new file mode 100644
index 0000000000..8ff7ce48e5
--- /dev/null
+++ b/activestorage/lib/active_storage/analyzer/null_analyzer.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module ActiveStorage
+ class Analyzer::NullAnalyzer < Analyzer # :nodoc:
+ def self.accept?(blob)
+ true
+ end
+
+ def metadata
+ {}
+ end
+ end
+end
diff --git a/activestorage/lib/active_storage/analyzer/video_analyzer.rb b/activestorage/lib/active_storage/analyzer/video_analyzer.rb
new file mode 100644
index 0000000000..408b5e58e9
--- /dev/null
+++ b/activestorage/lib/active_storage/analyzer/video_analyzer.rb
@@ -0,0 +1,79 @@
+# frozen_string_literal: true
+
+require "active_support/core_ext/hash/compact"
+
+module ActiveStorage
+ # Extracts the following from a video blob:
+ #
+ # * Width (pixels)
+ # * Height (pixels)
+ # * Duration (seconds)
+ # * Angle (degrees)
+ # * Aspect ratio
+ #
+ # Example:
+ #
+ # ActiveStorage::VideoAnalyzer.new(blob).metadata
+ # # => { width: 640, height: 480, duration: 5.0, angle: 0, aspect_ratio: [4, 3] }
+ #
+ # This analyzer requires the {ffmpeg}[https://www.ffmpeg.org] system library, which is not provided by Rails. You must
+ # install ffmpeg yourself to use this analyzer.
+ class Analyzer::VideoAnalyzer < Analyzer
+ def self.accept?(blob)
+ blob.video?
+ end
+
+ def metadata
+ { width: width, height: height, duration: duration, angle: angle, aspect_ratio: aspect_ratio }.compact
+ end
+
+ private
+ def width
+ Integer(video_stream["width"]) if video_stream["width"]
+ end
+
+ def height
+ Integer(video_stream["height"]) if video_stream["height"]
+ end
+
+ def duration
+ Float(video_stream["duration"]) if video_stream["duration"]
+ end
+
+ def angle
+ Integer(tags["rotate"]) if tags["rotate"]
+ end
+
+ def aspect_ratio
+ if descriptor = video_stream["display_aspect_ratio"]
+ descriptor.split(":", 2).collect(&:to_i)
+ end
+ end
+
+
+ def tags
+ @tags ||= video_stream["tags"] || {}
+ end
+
+ def video_stream
+ @video_stream ||= streams.detect { |stream| stream["codec_type"] == "video" } || {}
+ end
+
+ def streams
+ probe["streams"] || []
+ end
+
+ def probe
+ download_blob_to_tempfile { |file| probe_from(file) }
+ end
+
+ def probe_from(file)
+ IO.popen([ "ffprobe", "-print_format", "json", "-show_streams", "-v", "error", file.path ]) do |output|
+ JSON.parse(output.read)
+ end
+ rescue Errno::ENOENT
+ logger.info "Skipping video analysis because ffmpeg isn't installed"
+ {}
+ end
+ end
+end
diff --git a/activestorage/lib/active_storage/attached/one.rb b/activestorage/lib/active_storage/attached/one.rb
index c66be08f58..dc19512484 100644
--- a/activestorage/lib/active_storage/attached/one.rb
+++ b/activestorage/lib/active_storage/attached/one.rb
@@ -59,12 +59,16 @@ module ActiveStorage
def replace(attachable)
blob.tap do
transaction do
- destroy
+ destroy_attachment
write_attachment create_attachment_from(attachable)
end
end.purge_later
end
+ def destroy_attachment
+ attachment.destroy
+ end
+
def create_attachment_from(attachable)
ActiveStorage::Attachment.create!(record: record, name: name, blob: create_blob_from(attachable))
end
diff --git a/activestorage/lib/active_storage/downloading.rb b/activestorage/lib/active_storage/downloading.rb
new file mode 100644
index 0000000000..bcf42f610e
--- /dev/null
+++ b/activestorage/lib/active_storage/downloading.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+module ActiveStorage
+ module Downloading
+ private
+ # Opens a new tempfile and copies blob data into it. Yields the tempfile.
+ def download_blob_to_tempfile # :doc:
+ Tempfile.open("ActiveStorage") do |file|
+ download_blob_to file
+ yield file
+ end
+ end
+
+ # Efficiently download blob data into the given file.
+ def download_blob_to(file) # :doc:
+ file.binmode
+ blob.download { |chunk| file.write(chunk) }
+ file.rewind
+ end
+ end
+end
diff --git a/activestorage/lib/active_storage/engine.rb b/activestorage/lib/active_storage/engine.rb
index 335eae8dd8..a01a14cd83 100644
--- a/activestorage/lib/active_storage/engine.rb
+++ b/activestorage/lib/active_storage/engine.rb
@@ -6,20 +6,22 @@ require "active_storage"
require "active_storage/previewer/pdf_previewer"
require "active_storage/previewer/video_previewer"
+require "active_storage/analyzer/image_analyzer"
+require "active_storage/analyzer/video_analyzer"
+
module ActiveStorage
class Engine < Rails::Engine # :nodoc:
isolate_namespace ActiveStorage
config.active_storage = ActiveSupport::OrderedOptions.new
config.active_storage.previewers = [ ActiveStorage::Previewer::PDFPreviewer, ActiveStorage::Previewer::VideoPreviewer ]
+ config.active_storage.analyzers = [ ActiveStorage::Analyzer::ImageAnalyzer, ActiveStorage::Analyzer::VideoAnalyzer ]
config.eager_load_namespaces << ActiveStorage
initializer "active_storage.logger" do
- require "active_storage/service"
-
config.after_initialize do |app|
- ActiveStorage::Service.logger = app.config.active_storage.logger || Rails.logger
+ ActiveStorage.logger = app.config.active_storage.logger || Rails.logger
end
end
@@ -69,5 +71,11 @@ module ActiveStorage
ActiveStorage.previewers = app.config.active_storage.previewers || []
end
end
+
+ initializer "active_storage.analyzers" do
+ config.after_initialize do |app|
+ ActiveStorage.analyzers = app.config.active_storage.analyzers || []
+ end
+ end
end
end
diff --git a/activestorage/lib/active_storage/log_subscriber.rb b/activestorage/lib/active_storage/log_subscriber.rb
index 0d00a75c0e..5cbf4bd1a5 100644
--- a/activestorage/lib/active_storage/log_subscriber.rb
+++ b/activestorage/lib/active_storage/log_subscriber.rb
@@ -27,7 +27,7 @@ module ActiveStorage
end
def logger
- ActiveStorage::Service.logger
+ ActiveStorage.logger
end
private
diff --git a/activestorage/lib/active_storage/previewer.rb b/activestorage/lib/active_storage/previewer.rb
index 930b376067..460f6d5678 100644
--- a/activestorage/lib/active_storage/previewer.rb
+++ b/activestorage/lib/active_storage/previewer.rb
@@ -1,10 +1,14 @@
# frozen_string_literal: true
+require "active_storage/downloading"
+
module ActiveStorage
# This is an abstract base class for previewers, which generate images from blobs. See
# ActiveStorage::Previewer::PDFPreviewer and ActiveStorage::Previewer::VideoPreviewer for examples of
# concrete subclasses.
class Previewer
+ include Downloading
+
attr_reader :blob
# Implement this method in a concrete subclass. Have it return true when given a blob from which
@@ -24,37 +28,20 @@ module ActiveStorage
end
private
- # Downloads the blob to a new tempfile. Yields the tempfile.
- #
- # Use this method to get a tempfile that you can provide to a drawing command.
- def open # :doc:
- Tempfile.open("input") do |file|
- download_blob_to file
- yield file
- end
- end
-
- def download_blob_to(file)
- file.binmode
- blob.download { |chunk| file.write(chunk) }
- file.rewind
- end
-
-
# Executes a system command, capturing its binary output in a tempfile. Yields the tempfile.
#
- # Use this method to shell out to system libraries (e.g. mupdf or ffmpeg) for preview image
+ # Use this method to shell out to a system library (e.g. mupdf or ffmpeg) for preview image
# generation. The resulting tempfile can be used as the +:io+ value in an attachable Hash:
#
# def preview
- # open do |input|
+ # download_blob_to_tempfile do |input|
# draw "my-drawing-command", input.path, "--format", "png", "-" do |output|
# yield io: output, filename: "#{blob.filename.base}.png", content_type: "image/png"
# end
# end
# end
def draw(*argv) # :doc:
- Tempfile.open("output") do |file|
+ Tempfile.open("ActiveStorage") do |file|
capture(*argv, to: file)
yield file
end
diff --git a/activestorage/lib/active_storage/previewer/pdf_previewer.rb b/activestorage/lib/active_storage/previewer/pdf_previewer.rb
index 31a2a8f120..a2f05c74a6 100644
--- a/activestorage/lib/active_storage/previewer/pdf_previewer.rb
+++ b/activestorage/lib/active_storage/previewer/pdf_previewer.rb
@@ -7,7 +7,7 @@ module ActiveStorage
end
def preview
- open do |input|
+ download_blob_to_tempfile do |input|
draw "mutool", "draw", "-F", "png", "-o", "-", input.path, "1" do |output|
yield io: output, filename: "#{blob.filename.base}.png", content_type: "image/png"
end
diff --git a/activestorage/lib/active_storage/previewer/video_previewer.rb b/activestorage/lib/active_storage/previewer/video_previewer.rb
index 840d87f100..49f128d142 100644
--- a/activestorage/lib/active_storage/previewer/video_previewer.rb
+++ b/activestorage/lib/active_storage/previewer/video_previewer.rb
@@ -7,11 +7,17 @@ module ActiveStorage
end
def preview
- open do |input|
- draw "ffmpeg", "-i", input.path, "-y", "-vcodec", "png", "-vf", "thumbnail", "-vframes", "1", "-f", "image2", "-" do |output|
+ download_blob_to_tempfile do |input|
+ draw_relevant_frame_from input do |output|
yield io: output, filename: "#{blob.filename.base}.png", content_type: "image/png"
end
end
end
+
+ private
+ def draw_relevant_frame_from(file, &block)
+ draw "ffmpeg", "-i", file.path, "-y", "-vcodec", "png",
+ "-vf", "thumbnail", "-vframes", "1", "-f", "image2", "-", &block
+ end
end
end
diff --git a/activestorage/lib/active_storage/service.rb b/activestorage/lib/active_storage/service.rb
index 1f012da1e7..aa150e4d8a 100644
--- a/activestorage/lib/active_storage/service.rb
+++ b/activestorage/lib/active_storage/service.rb
@@ -41,8 +41,6 @@ module ActiveStorage
extend ActiveSupport::Autoload
autoload :Configurator
- class_attribute :logger
-
class_attribute :url_expires_in, default: 5.minutes
class << self