hydra-derivatives 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +3 -0
- data/.travis.yml +17 -0
- data/CONTRIBUTING.md +111 -0
- data/Gemfile +5 -0
- data/LICENSE.txt +15 -0
- data/README.md +4 -0
- data/Rakefile +24 -0
- data/TODO +2 -0
- data/VERSION +1 -0
- data/config/jetty.yml +6 -0
- data/hydra-derivatives.gemspec +29 -0
- data/lib/hydra/derivatives.rb +113 -0
- data/lib/hydra/derivatives/audio.rb +22 -0
- data/lib/hydra/derivatives/extract_metadata.rb +55 -0
- data/lib/hydra/derivatives/ffmpeg.rb +51 -0
- data/lib/hydra/derivatives/image.rb +53 -0
- data/lib/hydra/derivatives/processor.rb +38 -0
- data/lib/hydra/derivatives/video.rb +46 -0
- data/spec/fixtures/countdown.avi +0 -0
- data/spec/fixtures/piano_note.wav +0 -0
- data/spec/fixtures/test.pdf +0 -0
- data/spec/fixtures/world.png +0 -0
- data/spec/spec_helper.rb +9 -0
- data/spec/units/image_spec.rb +22 -0
- data/spec/units/transcoding_spec.rb +88 -0
- metadata +180 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 2760dd208eaff0215533b8885e35b115a78f1f4f
|
4
|
+
data.tar.gz: cdeab3f70511982e5440ad348856c0f51f7bc626
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: d8f7ec3e514d6d8b2e9aca347e1b2cff3e669e2d3de44bcdf24afd12790262f886d0b5ffb206c8ecea72b72d6151d850b0e7d65a2e5a092154c05b1eeff522f3
|
7
|
+
data.tar.gz: 0d15fefd69e56e2afc29806d3201e1787f9e00d41ef973bce43b57ca23965247f379346138caf6570d797573a1521db656f2b1d38ef3c5f358726e0e0d88f5c5
|
data/.gitignore
ADDED
data/.travis.yml
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
language: ruby
|
2
|
+
rvm:
|
3
|
+
- 1.9.3
|
4
|
+
- 2.0.0
|
5
|
+
notifications:
|
6
|
+
email:
|
7
|
+
recipients:
|
8
|
+
- "ul-dlt-applications@lists.psu.edu"
|
9
|
+
- "michael@psu.edu"
|
10
|
+
on_success: "change"
|
11
|
+
on_failure: "always"
|
12
|
+
irc:
|
13
|
+
channels:
|
14
|
+
- "irc.freenode.org#scholarsphere"
|
15
|
+
- "irc.freenode.org#projecthydra"
|
16
|
+
template:
|
17
|
+
- "%{repository}//%{branch}@%{commit} by %{author}: %{message} - %{build_url}"
|
data/CONTRIBUTING.md
ADDED
@@ -0,0 +1,111 @@
|
|
1
|
+
# How to Contribute
|
2
|
+
|
3
|
+
We want your help to make Project Hydra great.
|
4
|
+
There are a few guidelines that we need contributors to follow so that we can have a chance of keeping on top of things.
|
5
|
+
|
6
|
+
## Hydra Project Intellectual Property Licensing and Ownership
|
7
|
+
|
8
|
+
All code contributors must have an Individual Contributor License Agreement (iCLA) on file with the Hydra Project Steering Group.
|
9
|
+
If the contributor works for an institution, the institution must have a Corporate Contributor License Agreement (cCLA) on file.
|
10
|
+
|
11
|
+
https://wiki.duraspace.org/display/hydra/Hydra+Project+Intellectual+Property+Licensing+and+Ownership
|
12
|
+
|
13
|
+
You should also add yourself to the `CONTRIBUTORS.md` file in the root of the project.
|
14
|
+
|
15
|
+
## Contribution Tasks
|
16
|
+
|
17
|
+
* Reporting Issues
|
18
|
+
* Making Changes
|
19
|
+
* Submitting Changes
|
20
|
+
* Merging Changes
|
21
|
+
|
22
|
+
### Reporting Issues
|
23
|
+
|
24
|
+
* Make sure you have a [GitHub account](https://github.com/signup/free)
|
25
|
+
* Submit a [Github issue](./issues) by:
|
26
|
+
* Clearly describing the issue
|
27
|
+
* Provide a descriptive summary
|
28
|
+
* Explain the expected behavior
|
29
|
+
* Explain the actual behavior
|
30
|
+
* Provide steps to reproduce the actual behavior
|
31
|
+
|
32
|
+
### Making Changes
|
33
|
+
|
34
|
+
* Fork the repository on GitHub
|
35
|
+
* Create a topic branch from where you want to base your work.
|
36
|
+
* This is usually the master branch.
|
37
|
+
* To quickly create a topic branch based on master; `git branch fix/master/my_contribution master`
|
38
|
+
* Then checkout the new branch with `git checkout fix/master/my_contribution`.
|
39
|
+
* Please avoid working directly on the `master` branch.
|
40
|
+
* You may find the [hub suite of commands](https://github.com/defunkt/hub) helpful
|
41
|
+
* Make commits of logical units.
|
42
|
+
* Your commit should include a high level description of your work in HISTORY.textile
|
43
|
+
* Check for unnecessary whitespace with `git diff --check` before committing.
|
44
|
+
* Make sure your commit messages are [well formed](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html).
|
45
|
+
* If you created an issue, you can close it by including "Closes #issue" in your commit message. See [Github's blog post for more details](https://github.com/blog/1386-closing-issues-via-commit-messages)
|
46
|
+
|
47
|
+
```
|
48
|
+
Present tense short summary (50 characters or less)
|
49
|
+
|
50
|
+
More detailed description, if necessary. It should be wrapped to 72
|
51
|
+
characters. Try to be as descriptive as you can, even if you think that
|
52
|
+
the commit content is obvious, it may not be obvious to others. You
|
53
|
+
should add such description also if it's already present in bug tracker,
|
54
|
+
it should not be necessary to visit a webpage to check the history.
|
55
|
+
|
56
|
+
Include Closes #<issue-number> when relavent.
|
57
|
+
|
58
|
+
Description can have multiple paragraphs and you can use code examples
|
59
|
+
inside, just indent it with 4 spaces:
|
60
|
+
|
61
|
+
class PostsController
|
62
|
+
def index
|
63
|
+
respond_with Post.limit(10)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
You can also add bullet points:
|
68
|
+
|
69
|
+
- you can use dashes or asterisks
|
70
|
+
|
71
|
+
- also, try to indent next line of a point for readability, if it's too
|
72
|
+
long to fit in 72 characters
|
73
|
+
```
|
74
|
+
|
75
|
+
* Make sure you have added the necessary tests for your changes.
|
76
|
+
* Run _all_ the tests to assure nothing else was accidentally broken.
|
77
|
+
* When you are ready to submit a pull request
|
78
|
+
|
79
|
+
### Submitting Changes
|
80
|
+
|
81
|
+
* Read the article ["Using Pull Requests"](https://help.github.com/articles/using-pull-requests) on GitHub.
|
82
|
+
* Make sure your branch is up to date with its parent branch (i.e. master)
|
83
|
+
* `git checkout master`
|
84
|
+
* `git pull --rebase`
|
85
|
+
* `git checkout <your-branch>`
|
86
|
+
* `git rebase master`
|
87
|
+
* It is likely a good idea to run your tests again.
|
88
|
+
* Squash the commits for your branch into one commit
|
89
|
+
* `git rebase --interactive HEAD~<number-of-commits>` ([See Github help](https://help.github.com/articles/interactive-rebase))
|
90
|
+
* To determine the number of commits on your branch: `git log master..<your-branch> --oneline | wc -l`
|
91
|
+
* Squashing your branch's changes into one commit is "good form" and helps the person merging your request to see everything that is going on.
|
92
|
+
* Push your changes to a topic branch in your fork of the repository.
|
93
|
+
* Submit a pull request from your fork to the project.
|
94
|
+
|
95
|
+
### Merging Changes
|
96
|
+
|
97
|
+
* It is considered "poor from" to merge your own request.
|
98
|
+
* Please take the time to review the changes and get a sense of what is being changed. Things to consider:
|
99
|
+
* Does the commit message explain what is going on?
|
100
|
+
* Does the code changes have tests? _Not all changes need new tests, some changes are refactorings_
|
101
|
+
* Does the commit contain more than it should? Are two separate concerns being addressed in one commit?
|
102
|
+
* Did the Travis tests complete successfully?
|
103
|
+
* If you are uncertain, bring other contributors into the conversation by creating a comment that includes their @username.
|
104
|
+
* If you like the pull request, but want others to chime in, create a +1 comment and tag a user.
|
105
|
+
|
106
|
+
# Additional Resources
|
107
|
+
|
108
|
+
* [General GitHub documentation](http://help.github.com/)
|
109
|
+
* [GitHub pull request documentation](http://help.github.com/send-pull-requests/)
|
110
|
+
* [Pro Git](http://git-scm.com/book) is both a free and excellent book about Git.
|
111
|
+
* [A Git Config for Contributing](http://ndlib.github.io/practices/my-typical-per-project-git-config/)
|
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
##########################################################################
|
2
|
+
# Copyright 2013 Notre Dame, Data Curation Experts, WGBH
|
3
|
+
# Additional copyright may be held by others, as reflected in the commit log
|
4
|
+
#
|
5
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
6
|
+
# you may not use this file except in compliance with the License.
|
7
|
+
# You may obtain a copy of the License at
|
8
|
+
#
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
10
|
+
#
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
13
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
14
|
+
# See the License for the specific language governing permissions and
|
15
|
+
# limitations under the License.
|
data/README.md
ADDED
data/Rakefile
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
#!/usr/bin/env rake
|
2
|
+
|
3
|
+
require "bundler/gem_tasks"
|
4
|
+
|
5
|
+
APP_ROOT="." # for jettywrapper
|
6
|
+
require 'jettywrapper'
|
7
|
+
# Dir.glob('tasks/*.rake').each { |r| import r }
|
8
|
+
|
9
|
+
require 'rspec/core/rake_task'
|
10
|
+
|
11
|
+
RSpec::Core::RakeTask.new(:spec)
|
12
|
+
|
13
|
+
|
14
|
+
desc 'Spin up hydra-jetty and run specs'
|
15
|
+
task :ci => ['jetty:unzip'] do
|
16
|
+
puts 'running continuous integration'
|
17
|
+
jetty_params = Jettywrapper.load_config
|
18
|
+
error = Jettywrapper.wrap(jetty_params) do
|
19
|
+
Rake::Task['spec'].invoke
|
20
|
+
end
|
21
|
+
raise "test failures: #{error}" if error
|
22
|
+
end
|
23
|
+
|
24
|
+
task :default => :ci
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.0.1
|
data/config/jetty.yml
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
version = File.read(File.expand_path("../VERSION", __FILE__)).strip
|
3
|
+
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = "hydra-derivatives"
|
7
|
+
spec.version = version
|
8
|
+
spec.authors = ["Justin Coyne"]
|
9
|
+
spec.email = ["justin@curationexperts.com"]
|
10
|
+
spec.description = %q{Derivative generation plugin for hydra}
|
11
|
+
spec.summary = %q{Derivative generation plugin for hydra}
|
12
|
+
spec.license = "APACHE2"
|
13
|
+
spec.homepage = "https://github.com/projecthydra/hydra-derivatives"
|
14
|
+
|
15
|
+
spec.files = `git ls-files`.split($/)
|
16
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
17
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
18
|
+
spec.require_paths = ["lib"]
|
19
|
+
|
20
|
+
spec.add_development_dependency "bundler", "~> 1.3"
|
21
|
+
spec.add_development_dependency "rake"
|
22
|
+
spec.add_development_dependency "jettywrapper"
|
23
|
+
spec.add_development_dependency "rspec"
|
24
|
+
|
25
|
+
spec.add_dependency 'active-fedora'
|
26
|
+
spec.add_dependency 'rmagick'
|
27
|
+
spec.add_dependency 'activesupport', '>= 3.2.13', '< 5.0'
|
28
|
+
end
|
29
|
+
|
@@ -0,0 +1,113 @@
|
|
1
|
+
require 'active_fedora'
|
2
|
+
require 'RMagick'
|
3
|
+
module Hydra
|
4
|
+
module Derivatives
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
extend ActiveSupport::Autoload
|
7
|
+
|
8
|
+
autoload :Processor
|
9
|
+
autoload :Image
|
10
|
+
autoload :Ffmpeg
|
11
|
+
autoload :Video
|
12
|
+
autoload :Audio
|
13
|
+
autoload :ExtractMetadata
|
14
|
+
|
15
|
+
def self.ffmpeg_path=(val)
|
16
|
+
@ffmpeg_path = val
|
17
|
+
end
|
18
|
+
def self.ffmpeg_path
|
19
|
+
#Sufia.config.ffmpeg_path
|
20
|
+
@ffmpeg_path ||= 'ffmpeg'
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.temp_file_base=(val)
|
24
|
+
@temp_file_base = val
|
25
|
+
end
|
26
|
+
def self.temp_file_base
|
27
|
+
#Sufia.config.temp_file_base
|
28
|
+
@temp_file_base ||= '/tmp'
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.fits_path=(val)
|
32
|
+
@fits_path = val
|
33
|
+
end
|
34
|
+
def self.fits_path
|
35
|
+
#Sufia.config.fits_path
|
36
|
+
@fits_path ||= 'fits.sh'
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.enable_ffmpeg=(val)
|
40
|
+
@enable_ffmpeg = val
|
41
|
+
end
|
42
|
+
def self.enable_ffmpeg
|
43
|
+
@enable_ffmpeg ||= true
|
44
|
+
end
|
45
|
+
|
46
|
+
included do
|
47
|
+
class_attribute :transformation_scheme
|
48
|
+
end
|
49
|
+
|
50
|
+
|
51
|
+
# Runs all of the transformations immediately.
|
52
|
+
# You may want to run this job in the background as it may take a long time.
|
53
|
+
def create_derivatives
|
54
|
+
if transformation_scheme.present?
|
55
|
+
transformation_scheme.each do |datastream, value|
|
56
|
+
transform_datastream(datastream, value) if self.datastreams[datastream.to_s].has_content?
|
57
|
+
end
|
58
|
+
else
|
59
|
+
logger.warn "`create_derivatives' was called on an instance of #{self.class}, but no derivatives have been requested"
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
# Transform a single datastream
|
64
|
+
def transform_datastream(datastream, directive_list)
|
65
|
+
directive_list.each do |directive|
|
66
|
+
if directive.applies?(self)
|
67
|
+
processor = directive.processors ? Array(directive.processors).first : :image
|
68
|
+
"Hydra::Derivatives::#{processor.to_s.classify}".constantize.new(self, datastream, directive.derivatives).process
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
end
|
73
|
+
|
74
|
+
class TransformationDirective
|
75
|
+
attr_accessor :differentiator, :selector, :derivatives, :processors
|
76
|
+
# @param [Hash] args the options
|
77
|
+
# @option args [Symbol] :based_on the method that holds the differentiator column
|
78
|
+
# @option args [String, Array] :when activates this set of derivatives when the the differentiator column is includes one of these.
|
79
|
+
# @option args [Hash] :derivatives the derivatives to be produced
|
80
|
+
# @option args [Symbol, Array] :processors the processors to run to produce the derivatives
|
81
|
+
def initialize(args)
|
82
|
+
self.differentiator = args[:based_on]
|
83
|
+
self.selector = args[:when]
|
84
|
+
self.derivatives = args[:derivatives]
|
85
|
+
self.processors = args[:processors]
|
86
|
+
end
|
87
|
+
|
88
|
+
def applies?(object)
|
89
|
+
selector.include?(object.send(differentiator))
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
module ClassMethods
|
94
|
+
# @param [Symbol, String] datastream the datastream to operate on
|
95
|
+
# @param [Hash] args the options
|
96
|
+
# @option args [Symbol] :based_on the method that holds the differentiator column
|
97
|
+
# @option args [String, Array] :when activates this set of derivatives when the the differentiator column is includes one of these.
|
98
|
+
# @option args [Hash] :derivatives the derivatives to be produced
|
99
|
+
# @option args [Symbol, Array] :processors the processors to run to produce the derivatives
|
100
|
+
# @example
|
101
|
+
# makes_derivatives_of :content, based_on: :mime_type, when: 'text/pdf',
|
102
|
+
# derivatives: { :text => { :quality => :better }, processors: [:ocr]}
|
103
|
+
#
|
104
|
+
# makes_derivatives_of :content, based_on: :mime_type, when: ['image/png', 'image/jpg'],
|
105
|
+
# derivatives: { :medium => "300x300>", :thumb => "100x100>" }
|
106
|
+
def makes_derivatives_of(datastream, args = {})
|
107
|
+
self.transformation_scheme ||= {}
|
108
|
+
self.transformation_scheme[datastream.to_sym] ||= []
|
109
|
+
self.transformation_scheme[datastream.to_sym] << TransformationDirective.new(args)
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Hydra
|
2
|
+
module Derivatives
|
3
|
+
class Audio < Processor
|
4
|
+
include Ffmpeg
|
5
|
+
|
6
|
+
protected
|
7
|
+
|
8
|
+
def options_for(format)
|
9
|
+
end
|
10
|
+
|
11
|
+
def new_mime_type(format)
|
12
|
+
case format
|
13
|
+
when 'mp3'
|
14
|
+
"audio/mpeg"
|
15
|
+
else
|
16
|
+
"audio/#{format}"
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
@@ -0,0 +1,55 @@
|
|
1
|
+
require 'open3'
|
2
|
+
module Hydra
|
3
|
+
module Derivatives
|
4
|
+
module ExtractMetadata
|
5
|
+
include Open3
|
6
|
+
|
7
|
+
def extract_metadata
|
8
|
+
out = nil
|
9
|
+
to_tempfile do |f|
|
10
|
+
out = run_fits!(f.path)
|
11
|
+
end
|
12
|
+
out
|
13
|
+
end
|
14
|
+
|
15
|
+
def to_tempfile &block
|
16
|
+
return unless has_content?
|
17
|
+
tmp_base = Hydra::Derivatives.temp_file_base
|
18
|
+
f = Tempfile.new("#{pid}-#{dsVersionID}")
|
19
|
+
f.binmode
|
20
|
+
if content.respond_to? :read
|
21
|
+
f.write(content.read)
|
22
|
+
else
|
23
|
+
f.write(content)
|
24
|
+
end
|
25
|
+
f.close
|
26
|
+
content.rewind if content.respond_to? :rewind
|
27
|
+
yield(f)
|
28
|
+
f.unlink
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
|
34
|
+
def run_fits!(file_path)
|
35
|
+
command = "#{fits_path} -i \"#{file_path}\""
|
36
|
+
stdin, stdout, stderr, wait_thr = popen3(command)
|
37
|
+
stdin.close
|
38
|
+
out = stdout.read
|
39
|
+
stdout.close
|
40
|
+
err = stderr.read
|
41
|
+
stderr.close
|
42
|
+
exit_status = wait_thr.value
|
43
|
+
raise "Unable to execute command \"#{command}\"\n#{err}" unless exit_status.success?
|
44
|
+
out
|
45
|
+
end
|
46
|
+
|
47
|
+
|
48
|
+
def fits_path
|
49
|
+
Hydra::Derivatives.fits_path
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
@@ -0,0 +1,51 @@
|
|
1
|
+
# An abstract class for asyncronous jobs that transcode files using FFMpeg
|
2
|
+
|
3
|
+
require 'tmpdir'
|
4
|
+
require 'open3'
|
5
|
+
|
6
|
+
module Hydra
|
7
|
+
module Derivatives
|
8
|
+
module Ffmpeg
|
9
|
+
extend ActiveSupport::Concern
|
10
|
+
|
11
|
+
included do
|
12
|
+
extend Open3
|
13
|
+
end
|
14
|
+
|
15
|
+
def process
|
16
|
+
directives.each do |name, args|
|
17
|
+
raise ArgumentError, "You must provide the :format you want to transcode into. You provided #{args}" unless args[:format]
|
18
|
+
# TODO if the source is in the correct format, we could just copy it and skip transcoding.
|
19
|
+
encode_datastream(output_datastream_id(name), args[:format], new_mime_type(args[:format]), options_for(args[:format]))
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def encode_datastream(dest_dsid, file_suffix, mime_type, options = '')
|
24
|
+
out_file = nil
|
25
|
+
output_file = Dir::Tmpname.create(['sufia', ".#{file_suffix}"], Hydra::Derivatives.temp_file_base){}
|
26
|
+
source_datastream.to_tempfile do |f|
|
27
|
+
self.class.encode(f.path, options, output_file)
|
28
|
+
end
|
29
|
+
out_file = File.open(output_file, "rb")
|
30
|
+
object.add_file_datastream(out_file.read, :dsid=>dest_dsid, :mimeType=>mime_type)
|
31
|
+
File.unlink(output_file)
|
32
|
+
end
|
33
|
+
|
34
|
+
module ClassMethods
|
35
|
+
|
36
|
+
def encode(path, options, output_file)
|
37
|
+
command = "#{Hydra::Derivatives.ffmpeg_path} -y -i \"#{path}\" #{options} #{output_file}"
|
38
|
+
stdin, stdout, stderr, wait_thr = popen3(command)
|
39
|
+
stdin.close
|
40
|
+
out = stdout.read
|
41
|
+
stdout.close
|
42
|
+
err = stderr.read
|
43
|
+
stderr.close
|
44
|
+
raise "Unable to execute command \"#{command}\"\n#{err}" unless wait_thr.value.success?
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module Hydra
|
2
|
+
module Derivatives
|
3
|
+
class Image < Processor
|
4
|
+
def process
|
5
|
+
directives.each do |name, args|
|
6
|
+
size = args
|
7
|
+
output_datastream_name = output_datastream_id(name)
|
8
|
+
if args.kind_of? Hash
|
9
|
+
size = args[:size]
|
10
|
+
output_datastream_name = args[:datastream] if args[:datastream]
|
11
|
+
end
|
12
|
+
create_resized_image(output_datastream_name, size, new_mime_type)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
|
17
|
+
protected
|
18
|
+
|
19
|
+
def new_mime_type
|
20
|
+
'image/png'
|
21
|
+
end
|
22
|
+
|
23
|
+
|
24
|
+
def create_resized_image(output_dsid, size, mime_type, quality=nil)
|
25
|
+
create_image(output_dsid, mime_type, quality) do |xfrm|
|
26
|
+
xfrm.change_geometry!(size) do |cols, rows, img|
|
27
|
+
img.resize!(cols, rows)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def create_image(dsid, mime_type, quality=nil)
|
33
|
+
xfrm = load_image_transformer
|
34
|
+
yield(xfrm) if block_given?
|
35
|
+
#out = output_datastream
|
36
|
+
output_datastream(dsid).content = if quality
|
37
|
+
xfrm.to_blob { self.quality = quality }
|
38
|
+
else
|
39
|
+
xfrm.to_blob
|
40
|
+
end
|
41
|
+
output_datastream(dsid).mimeType = mime_type
|
42
|
+
end
|
43
|
+
|
44
|
+
# Override this method if you want a different transformer, or need to load the
|
45
|
+
# raw image from a different source (e.g. external datastream)
|
46
|
+
def load_image_transformer
|
47
|
+
Magick::ImageList.new.tap do |xformer|
|
48
|
+
xformer.from_blob(source_datastream.content)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module Hydra
|
2
|
+
module Derivatives
|
3
|
+
class Processor
|
4
|
+
attr_accessor :object, :source_name, :directives
|
5
|
+
|
6
|
+
def initialize(obj, source_name, directives)
|
7
|
+
self.object = obj
|
8
|
+
self.source_name = source_name
|
9
|
+
self.directives = directives
|
10
|
+
end
|
11
|
+
|
12
|
+
def process
|
13
|
+
raise "Processor is an abstract class. Implement `process' on #{self.class.name}"
|
14
|
+
end
|
15
|
+
|
16
|
+
def output_datastream_id(name)
|
17
|
+
[source_name, name].join('_')
|
18
|
+
end
|
19
|
+
|
20
|
+
def output_datastream(dsid)
|
21
|
+
# first, check for a defined datastream
|
22
|
+
output_datastream = if object.datastreams[dsid]
|
23
|
+
object.datastreams[dsid]
|
24
|
+
else
|
25
|
+
ds = ActiveFedora::Datastream.new(object.inner_object, dsid)
|
26
|
+
object.add_datastream(ds)
|
27
|
+
ds
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def source_datastream
|
32
|
+
object.datastreams[source_name.to_s]
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module Hydra
|
2
|
+
module Derivatives
|
3
|
+
class Video < Processor
|
4
|
+
include Ffmpeg
|
5
|
+
|
6
|
+
protected
|
7
|
+
|
8
|
+
def options_for(format)
|
9
|
+
"-s #{size_attributes} #{video_attributes} #{codecs(format)} #{audio_attributes}"
|
10
|
+
end
|
11
|
+
|
12
|
+
def video_bitrate
|
13
|
+
'345k'
|
14
|
+
end
|
15
|
+
|
16
|
+
def video_attributes
|
17
|
+
"-g 30 -b:v #{video_bitrate}"
|
18
|
+
end
|
19
|
+
|
20
|
+
def size_attributes
|
21
|
+
"320x240"
|
22
|
+
end
|
23
|
+
|
24
|
+
def audio_attributes
|
25
|
+
"-ac 2 -ab 96k -ar 44100"
|
26
|
+
end
|
27
|
+
|
28
|
+
def codecs(format)
|
29
|
+
case format
|
30
|
+
when 'mp4'
|
31
|
+
"-vcodec libx264 -acodec libfaac"
|
32
|
+
when 'webm'
|
33
|
+
"-acodec libvorbis"
|
34
|
+
else
|
35
|
+
raise ArgumentError, "Unknown format `#{format}'"
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def new_mime_type(format)
|
40
|
+
"video/#{format}"
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Hydra::Derivatives::Image do
|
4
|
+
describe "when arguments are passed as a string" do
|
5
|
+
let(:directives) {{ :thumb => "100x100>" } }
|
6
|
+
subject { Hydra::Derivatives::Image.new(double(:obj), 'content', directives)}
|
7
|
+
|
8
|
+
it "should use the string as the size and the name is autogenerated" do
|
9
|
+
subject.should_receive(:create_resized_image).with("content_thumb", "100x100>", 'image/png')
|
10
|
+
subject.process
|
11
|
+
end
|
12
|
+
end
|
13
|
+
describe "when arguments are passed as a hash" do
|
14
|
+
let(:directives) {{ :thumb => {size: "200x300>", datastream: 'thumbnail'} }}
|
15
|
+
subject { Hydra::Derivatives::Image.new(double(:obj), 'content', directives)}
|
16
|
+
it "should use the specified size and name" do
|
17
|
+
subject.should_receive(:create_resized_image).with("thumbnail", "200x300>", 'image/png')
|
18
|
+
subject.process
|
19
|
+
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe "Transcoder" do
|
4
|
+
before do
|
5
|
+
class ContentDatastream < ActiveFedora::Datastream
|
6
|
+
include Hydra::Derivatives::ExtractMetadata
|
7
|
+
end
|
8
|
+
class GenericFile < ActiveFedora::Base
|
9
|
+
include Hydra::Derivatives
|
10
|
+
has_metadata 'characterization', type: ActiveFedora::SimpleDatastream do |m|
|
11
|
+
m.field "mime_type", :string
|
12
|
+
end
|
13
|
+
|
14
|
+
delegate :mime_type, :to => :characterization, :unique => true
|
15
|
+
has_file_datastream 'content', type: ContentDatastream
|
16
|
+
|
17
|
+
makes_derivatives_of :content, based_on: :mime_type, when: 'application/pdf',
|
18
|
+
derivatives: { :thumb => "100x100>" }
|
19
|
+
|
20
|
+
makes_derivatives_of :content, based_on: :mime_type, when: 'audio/wav',
|
21
|
+
derivatives: { :mp3 => {format: 'mp3'}, :ogg => {format: 'ogg'} }, processors: :audio
|
22
|
+
|
23
|
+
# -g 30 enforces keyframe generation every second (30fps)
|
24
|
+
# -b:v is the video bitrate
|
25
|
+
# -acodec is the audio codec
|
26
|
+
size_attributes = "-s 320x240"
|
27
|
+
audio_attributes = "-ac 2 -ab 96k -ar 44100"
|
28
|
+
makes_derivatives_of :content, based_on: :mime_type, when: 'video/avi',
|
29
|
+
derivatives: { :mp4 => {format: 'mp4'}, :webm => {format: 'webm'} }, processors: :video
|
30
|
+
|
31
|
+
makes_derivatives_of :content, based_on: :mime_type, when: ['image/png', 'image/jpg'],
|
32
|
+
derivatives: { :medium => "300x300>", :thumb => "100x100>" }
|
33
|
+
|
34
|
+
end
|
35
|
+
end
|
36
|
+
describe "with an attached image" do
|
37
|
+
let(:attachment) { File.open(File.expand_path('../../fixtures/world.png', __FILE__))}
|
38
|
+
let(:file) { GenericFile.new(mime_type: 'image/png').tap { |t| t.content.content = attachment; t.save } }
|
39
|
+
|
40
|
+
it "should transcode" do
|
41
|
+
file.datastreams.key?('content_medium').should be_false
|
42
|
+
file.create_derivatives
|
43
|
+
file.datastreams['content_medium'].should have_content
|
44
|
+
file.datastreams['content_medium'].mimeType.should == 'image/png'
|
45
|
+
file.datastreams['content_thumb'].should have_content
|
46
|
+
file.datastreams['content_thumb'].mimeType.should == 'image/png'
|
47
|
+
file.datastreams.key?('content_text').should be_false
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
describe "with an attached pdf" do
|
52
|
+
let(:attachment) { File.open(File.expand_path('../../fixtures/test.pdf', __FILE__))}
|
53
|
+
let(:file) { GenericFile.new(mime_type: 'application/pdf').tap { |t| t.content.content = attachment; t.save } }
|
54
|
+
|
55
|
+
it "should transcode" do
|
56
|
+
file.datastreams.key?('content_thumb').should be_false
|
57
|
+
file.create_derivatives
|
58
|
+
file.datastreams['content_thumb'].should have_content
|
59
|
+
file.datastreams['content_thumb'].mimeType.should == 'image/png'
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
describe "with an attached audio", unless: ENV['TRAVIS'] == 'true' do
|
64
|
+
let(:attachment) { File.open(File.expand_path('../../fixtures/piano_note.wav', __FILE__))}
|
65
|
+
let(:file) { GenericFile.new(mime_type: 'audio/wav').tap { |t| t.content.content = attachment; t.save } }
|
66
|
+
|
67
|
+
it "should transcode" do
|
68
|
+
file.create_derivatives
|
69
|
+
file.datastreams['content_mp3'].should have_content
|
70
|
+
file.datastreams['content_mp3'].mimeType.should == 'audio/mpeg'
|
71
|
+
file.datastreams['content_ogg'].should have_content
|
72
|
+
file.datastreams['content_ogg'].mimeType.should == 'audio/ogg'
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
describe "with an attached video", unless: ENV['TRAVIS'] == 'true' do
|
77
|
+
let(:attachment) { File.open(File.expand_path('../../fixtures/countdown.avi', __FILE__))}
|
78
|
+
let(:file) { GenericFile.new(mime_type: 'video/avi').tap { |t| t.content.content = attachment; t.save } }
|
79
|
+
|
80
|
+
it "should transcode" do
|
81
|
+
file.create_derivatives
|
82
|
+
file.datastreams['content_mp4'].should have_content
|
83
|
+
file.datastreams['content_mp4'].mimeType.should == 'video/mp4'
|
84
|
+
file.datastreams['content_webm'].should have_content
|
85
|
+
file.datastreams['content_webm'].mimeType.should == 'video/webm'
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
metadata
ADDED
@@ -0,0 +1,180 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: hydra-derivatives
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Justin Coyne
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2013-07-23 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ~>
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.3'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ~>
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.3'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - '>='
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - '>='
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: jettywrapper
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - '>='
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - '>='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rspec
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - '>='
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: active-fedora
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - '>='
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :runtime
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - '>='
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: rmagick
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - '>='
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :runtime
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - '>='
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: activesupport
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - '>='
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: 3.2.13
|
104
|
+
- - <
|
105
|
+
- !ruby/object:Gem::Version
|
106
|
+
version: '5.0'
|
107
|
+
type: :runtime
|
108
|
+
prerelease: false
|
109
|
+
version_requirements: !ruby/object:Gem::Requirement
|
110
|
+
requirements:
|
111
|
+
- - '>='
|
112
|
+
- !ruby/object:Gem::Version
|
113
|
+
version: 3.2.13
|
114
|
+
- - <
|
115
|
+
- !ruby/object:Gem::Version
|
116
|
+
version: '5.0'
|
117
|
+
description: Derivative generation plugin for hydra
|
118
|
+
email:
|
119
|
+
- justin@curationexperts.com
|
120
|
+
executables: []
|
121
|
+
extensions: []
|
122
|
+
extra_rdoc_files: []
|
123
|
+
files:
|
124
|
+
- .gitignore
|
125
|
+
- .travis.yml
|
126
|
+
- CONTRIBUTING.md
|
127
|
+
- Gemfile
|
128
|
+
- LICENSE.txt
|
129
|
+
- README.md
|
130
|
+
- Rakefile
|
131
|
+
- TODO
|
132
|
+
- VERSION
|
133
|
+
- config/jetty.yml
|
134
|
+
- hydra-derivatives.gemspec
|
135
|
+
- lib/hydra/derivatives.rb
|
136
|
+
- lib/hydra/derivatives/audio.rb
|
137
|
+
- lib/hydra/derivatives/extract_metadata.rb
|
138
|
+
- lib/hydra/derivatives/ffmpeg.rb
|
139
|
+
- lib/hydra/derivatives/image.rb
|
140
|
+
- lib/hydra/derivatives/processor.rb
|
141
|
+
- lib/hydra/derivatives/video.rb
|
142
|
+
- spec/fixtures/countdown.avi
|
143
|
+
- spec/fixtures/piano_note.wav
|
144
|
+
- spec/fixtures/test.pdf
|
145
|
+
- spec/fixtures/world.png
|
146
|
+
- spec/spec_helper.rb
|
147
|
+
- spec/units/image_spec.rb
|
148
|
+
- spec/units/transcoding_spec.rb
|
149
|
+
homepage: https://github.com/projecthydra/hydra-derivatives
|
150
|
+
licenses:
|
151
|
+
- APACHE2
|
152
|
+
metadata: {}
|
153
|
+
post_install_message:
|
154
|
+
rdoc_options: []
|
155
|
+
require_paths:
|
156
|
+
- lib
|
157
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
158
|
+
requirements:
|
159
|
+
- - '>='
|
160
|
+
- !ruby/object:Gem::Version
|
161
|
+
version: '0'
|
162
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
163
|
+
requirements:
|
164
|
+
- - '>='
|
165
|
+
- !ruby/object:Gem::Version
|
166
|
+
version: '0'
|
167
|
+
requirements: []
|
168
|
+
rubyforge_project:
|
169
|
+
rubygems_version: 2.0.5
|
170
|
+
signing_key:
|
171
|
+
specification_version: 4
|
172
|
+
summary: Derivative generation plugin for hydra
|
173
|
+
test_files:
|
174
|
+
- spec/fixtures/countdown.avi
|
175
|
+
- spec/fixtures/piano_note.wav
|
176
|
+
- spec/fixtures/test.pdf
|
177
|
+
- spec/fixtures/world.png
|
178
|
+
- spec/spec_helper.rb
|
179
|
+
- spec/units/image_spec.rb
|
180
|
+
- spec/units/transcoding_spec.rb
|