multi_exiftool 0.0.1 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +1 -1
- data/LICENSE +21 -0
- data/Manifest +16 -23
- data/README +77 -16
- data/Rakefile +17 -38
- data/examples/01_simple_reading.rb +13 -0
- data/examples/02_simple_writing.rb +19 -0
- data/examples/03_reading_using_groups.rb +28 -0
- data/lib/multi_exiftool.rb +6 -38
- data/lib/multi_exiftool/executable.rb +67 -0
- data/lib/multi_exiftool/reader.rb +60 -0
- data/lib/multi_exiftool/values.rb +60 -0
- data/lib/multi_exiftool/writer.rb +55 -0
- data/multi_exiftool.gemspec +25 -16
- data/test/helper.rb +27 -0
- data/test/test_reader.rb +135 -0
- data/test/test_values.rb +75 -0
- data/test/test_values_using_groups.rb +61 -0
- data/test/test_writer.rb +102 -0
- metadata +49 -47
- data/COPYING +0 -165
- data/data/fixtures/read_non_existing_file.stderr +0 -1
- data/data/fixtures/read_non_existing_file.stdout +0 -0
- data/data/fixtures/read_one_file.stderr +0 -0
- data/data/fixtures/read_one_file.stdout +0 -92
- data/data/fixtures/read_two_files.stderr +0 -0
- data/data/fixtures/read_two_files.stdout +0 -212
- data/data/regression/read_command.rb +0 -12
- data/data/regression/read_command.rb.out +0 -16
- data/data/regression/write_command.rb +0 -16
- data/data/regression/write_command.rb.out +0 -40
- data/lib/multi_exiftool/command_generator.rb +0 -68
- data/lib/multi_exiftool/parser.rb +0 -43
- data/lib/multi_exiftool/read_object.rb +0 -57
- data/script/colorize.rb +0 -6
- data/script/generate_fixture.rb +0 -27
- data/test/test_command_generator.rb +0 -89
- data/test/test_helper.rb +0 -44
- data/test/test_parser.rb +0 -51
- data/test/test_read_object.rb +0 -36
@@ -0,0 +1,60 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
require 'date'
|
3
|
+
module MultiExiftool
|
4
|
+
|
5
|
+
# Representing (tag, value) pairs of metadata.
|
6
|
+
# Access via bracket-methods or dynamic method-interpreting via
|
7
|
+
# method_missing.
|
8
|
+
class Values
|
9
|
+
|
10
|
+
def initialize values
|
11
|
+
@values = {}
|
12
|
+
values.map do |tag,val|
|
13
|
+
val = val.kind_of?(Hash) ? Values.new(val) : val
|
14
|
+
@values[Values.unify_tag(tag)] = val
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def [](tag)
|
19
|
+
parse_value(@values[Values.unify_tag(tag)])
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.unify_tag tag
|
23
|
+
tag.gsub(/[-_]/, '').downcase
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def method_missing tag, *args, &block
|
29
|
+
res = self[Values.unify_tag(tag.to_s)]
|
30
|
+
if res && block_given?
|
31
|
+
if block.arity > 0
|
32
|
+
yield res
|
33
|
+
else
|
34
|
+
res.instance_eval &block
|
35
|
+
end
|
36
|
+
end
|
37
|
+
res
|
38
|
+
end
|
39
|
+
|
40
|
+
def parse_value val
|
41
|
+
return val unless val.kind_of?(String)
|
42
|
+
case val
|
43
|
+
when /^(\d{4}):(\d\d):(\d\d) (\d\d):(\d\d):(\d\d)([-+]\d\d:\d\d)?$/
|
44
|
+
arr = $~.captures[0,6].map {|cap| cap.to_i}
|
45
|
+
arr << $7 if $7
|
46
|
+
if arr.size == 7
|
47
|
+
DateTime.new(*arr).to_time
|
48
|
+
else
|
49
|
+
Time.local(*arr)
|
50
|
+
end
|
51
|
+
when %r(^(\d+)/(\d+)$)
|
52
|
+
Rational($1, $2)
|
53
|
+
else
|
54
|
+
val
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
require_relative 'executable'
|
3
|
+
|
4
|
+
module MultiExiftool
|
5
|
+
|
6
|
+
# Handle writing of metadata via exiftool.
|
7
|
+
# Composing the command for the command-line executing it and parsing
|
8
|
+
# possible errors.
|
9
|
+
class Writer
|
10
|
+
|
11
|
+
attr_accessor :overwrite_original
|
12
|
+
attr_writer :values
|
13
|
+
|
14
|
+
include Executable
|
15
|
+
|
16
|
+
def values
|
17
|
+
Array(@values)
|
18
|
+
end
|
19
|
+
|
20
|
+
# Options to use with the exiftool command.
|
21
|
+
def options
|
22
|
+
opts = super
|
23
|
+
opts[:overwrite_original] = true if @overwrite_original
|
24
|
+
opts
|
25
|
+
end
|
26
|
+
|
27
|
+
# Getting the command for the command-line which would be executed
|
28
|
+
# when calling #write. It could be useful for logging, debugging or
|
29
|
+
# maybe even for creating a batch-file with exiftool command to be
|
30
|
+
# processed.
|
31
|
+
def command
|
32
|
+
cmd = [exiftool_command]
|
33
|
+
cmd << options_args
|
34
|
+
cmd << values_args
|
35
|
+
cmd << escaped_filenames
|
36
|
+
cmd.flatten.join(' ')
|
37
|
+
end
|
38
|
+
|
39
|
+
alias write execute # :nodoc:
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def values_args
|
44
|
+
raise MultiExiftool::Error.new('No values.') if values.empty?
|
45
|
+
@values.map {|tag, val| "-#{tag}=#{escape(val.to_s)}"}
|
46
|
+
end
|
47
|
+
|
48
|
+
def parse_results
|
49
|
+
@errors = @stderr.readlines
|
50
|
+
@errors.empty?
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
data/multi_exiftool.gemspec
CHANGED
@@ -2,34 +2,43 @@
|
|
2
2
|
|
3
3
|
Gem::Specification.new do |s|
|
4
4
|
s.name = %q{multi_exiftool}
|
5
|
-
s.version = "0.0
|
5
|
+
s.version = "0.1.0"
|
6
6
|
|
7
7
|
s.required_rubygems_version = Gem::Requirement.new(">= 1.2") if s.respond_to? :required_rubygems_version=
|
8
8
|
s.authors = ["Jan Friedrich"]
|
9
|
-
s.date = %q{
|
10
|
-
s.description = %q{}
|
11
|
-
s.email = %q{
|
12
|
-
s.extra_rdoc_files = ["lib/multi_exiftool.rb", "lib/multi_exiftool/
|
13
|
-
s.files = ["
|
14
|
-
s.
|
15
|
-
s.
|
9
|
+
s.date = %q{2010-01-27}
|
10
|
+
s.description = %q{This library is wrapper for the Exiftool command-line application (http://www.sno.phy.queensu.ca/~phil/exiftool) written by Phil Harvey. It is designed for dealing with multiple files at once by creating commands to call exiftool with various arguments, call it and parsing the results.}
|
11
|
+
s.email = %q{janfri26@gmail.com}
|
12
|
+
s.extra_rdoc_files = ["CHANGELOG", "LICENSE", "README", "lib/multi_exiftool.rb", "lib/multi_exiftool/executable.rb", "lib/multi_exiftool/reader.rb", "lib/multi_exiftool/values.rb", "lib/multi_exiftool/writer.rb"]
|
13
|
+
s.files = ["CHANGELOG", "LICENSE", "Manifest", "README", "Rakefile", "examples/01_simple_reading.rb", "examples/02_simple_writing.rb", "examples/03_reading_using_groups.rb", "lib/multi_exiftool.rb", "lib/multi_exiftool/executable.rb", "lib/multi_exiftool/reader.rb", "lib/multi_exiftool/values.rb", "lib/multi_exiftool/writer.rb", "test/helper.rb", "test/test_reader.rb", "test/test_values.rb", "test/test_values_using_groups.rb", "test/test_writer.rb", "multi_exiftool.gemspec"]
|
14
|
+
s.homepage = %q{http://rubyforge.org/projects/multiexiftool}
|
15
|
+
s.post_install_message = %q{
|
16
|
+
+-----------------------------------------------------------------------+
|
17
|
+
| Please ensure you have installed exiftool version 7.65 or higher and |
|
18
|
+
| it's found in your PATH (Try "exiftool -ver" on your commandline). |
|
19
|
+
| For more details see |
|
20
|
+
| http://www.sno.phy.queensu.ca/~phil/exiftool/install.html |
|
21
|
+
+-----------------------------------------------------------------------+
|
22
|
+
}
|
16
23
|
s.rdoc_options = ["--line-numbers", "--inline-source", "--title", "Multi_exiftool", "--main", "README"]
|
17
24
|
s.require_paths = ["lib"]
|
18
|
-
s.
|
19
|
-
s.
|
20
|
-
s.
|
21
|
-
s.
|
25
|
+
s.required_ruby_version = Gem::Requirement.new(">= 1.9.1")
|
26
|
+
s.requirements = ["exiftool, version 7.65 or higher"]
|
27
|
+
s.rubyforge_project = %q{multi_exiftool}
|
28
|
+
s.rubygems_version = %q{1.3.5}
|
29
|
+
s.summary = %q{This library is wrapper for the Exiftool command-line application (http://www.sno.phy.queensu.ca/~phil/exiftool).}
|
30
|
+
s.test_files = ["test/test_reader.rb", "test/test_values.rb", "test/test_values_using_groups.rb", "test/test_writer.rb"]
|
22
31
|
|
23
32
|
if s.respond_to? :specification_version then
|
24
33
|
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
25
|
-
s.specification_version =
|
34
|
+
s.specification_version = 3
|
26
35
|
|
27
36
|
if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
|
28
|
-
s.add_development_dependency(%q<
|
37
|
+
s.add_development_dependency(%q<contest>, [">= 0"])
|
29
38
|
else
|
30
|
-
s.add_dependency(%q<
|
39
|
+
s.add_dependency(%q<contest>, [">= 0"])
|
31
40
|
end
|
32
41
|
else
|
33
|
-
s.add_dependency(%q<
|
42
|
+
s.add_dependency(%q<contest>, [">= 0"])
|
34
43
|
end
|
35
44
|
end
|
data/test/helper.rb
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
require_relative '../lib/multi_exiftool'
|
3
|
+
require 'test/unit'
|
4
|
+
require 'contest'
|
5
|
+
require 'open3'
|
6
|
+
require 'stringio'
|
7
|
+
|
8
|
+
module TestHelper
|
9
|
+
|
10
|
+
def mocking_open3(command, outstr, errstr)
|
11
|
+
open3_eigenclass = class << Open3; self; end
|
12
|
+
open3_eigenclass.module_exec(command, outstr, errstr) do |cmd, out, err|
|
13
|
+
define_method :popen3 do |arg|
|
14
|
+
if arg == cmd
|
15
|
+
return [nil, StringIO.new(out), StringIO.new(err)]
|
16
|
+
else
|
17
|
+
raise ArgumentError.new("Expected call of Open3.popen3 with argument #{cmd.inspect} but was #{arg.inspect}.")
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
|
25
|
+
class Test::Unit::TestCase
|
26
|
+
include TestHelper
|
27
|
+
end
|
data/test/test_reader.rb
ADDED
@@ -0,0 +1,135 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
require_relative 'helper'
|
3
|
+
|
4
|
+
class TestReader < Test::Unit::TestCase
|
5
|
+
|
6
|
+
setup do
|
7
|
+
@reader = MultiExiftool::Reader.new
|
8
|
+
end
|
9
|
+
|
10
|
+
context 'command method' do
|
11
|
+
|
12
|
+
test 'simple case' do
|
13
|
+
@reader.filenames = %w(a.jpg b.tif c.bmp)
|
14
|
+
command = 'exiftool -J a.jpg b.tif c.bmp'
|
15
|
+
assert_equal command, @reader.command
|
16
|
+
end
|
17
|
+
|
18
|
+
test 'no filenames' do
|
19
|
+
assert_raises MultiExiftool::Error do
|
20
|
+
@reader.command
|
21
|
+
end
|
22
|
+
@reader.filenames = []
|
23
|
+
assert_raises MultiExiftool::Error do
|
24
|
+
@reader.command
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
test 'filenames with spaces' do
|
29
|
+
@reader.filenames = ['one file with spaces.jpg', 'another file with spaces.tif']
|
30
|
+
command = 'exiftool -J one\ file\ with\ spaces.jpg another\ file\ with\ spaces.tif'
|
31
|
+
assert_equal command, @reader.command
|
32
|
+
end
|
33
|
+
|
34
|
+
test 'tags' do
|
35
|
+
@reader.filenames = %w(a.jpg b.tif c.bmp)
|
36
|
+
@reader.tags = %w(author fnumber)
|
37
|
+
command = 'exiftool -J -author -fnumber a.jpg b.tif c.bmp'
|
38
|
+
assert_equal command, @reader.command
|
39
|
+
end
|
40
|
+
|
41
|
+
test 'options with boolean argument' do
|
42
|
+
@reader.filenames = %w(a.jpg b.tif c.bmp)
|
43
|
+
@reader.options = {:e => true}
|
44
|
+
command = 'exiftool -J -e a.jpg b.tif c.bmp'
|
45
|
+
assert_equal command, @reader.command
|
46
|
+
end
|
47
|
+
|
48
|
+
test 'options with value argument' do
|
49
|
+
@reader.filenames = %w(a.jpg b.tif c.bmp)
|
50
|
+
@reader.options = {:lang => 'de'}
|
51
|
+
command = 'exiftool -J -lang de a.jpg b.tif c.bmp'
|
52
|
+
assert_equal command, @reader.command
|
53
|
+
end
|
54
|
+
|
55
|
+
test 'numerical flag' do
|
56
|
+
@reader.filenames = %w(a.jpg b.tif c.bmp)
|
57
|
+
@reader.numerical = true
|
58
|
+
command = 'exiftool -J -n a.jpg b.tif c.bmp'
|
59
|
+
assert_equal command, @reader.command
|
60
|
+
end
|
61
|
+
|
62
|
+
test 'group flag' do
|
63
|
+
@reader.filenames = %w(a.jpg)
|
64
|
+
@reader.group = 0
|
65
|
+
command = 'exiftool -J -g0 a.jpg'
|
66
|
+
assert_equal command, @reader.command
|
67
|
+
@reader.group = 1
|
68
|
+
command = 'exiftool -J -g1 a.jpg'
|
69
|
+
assert_equal command, @reader.command
|
70
|
+
end
|
71
|
+
|
72
|
+
end
|
73
|
+
|
74
|
+
context 'read method' do
|
75
|
+
|
76
|
+
test 'try to read a non-existing file' do
|
77
|
+
mocking_open3('exiftool -J non_existing_file', '', 'File non_existing_file not found.')
|
78
|
+
@reader.filenames = %w(non_existing_file)
|
79
|
+
res = @reader.read
|
80
|
+
assert_equal [], res
|
81
|
+
assert_equal ['File non_existing_file not found.'], @reader.errors
|
82
|
+
end
|
83
|
+
|
84
|
+
test 'successful reading with one tag' do
|
85
|
+
json = <<-EOS
|
86
|
+
[{
|
87
|
+
"SourceFile": "a.jpg",
|
88
|
+
"FNumber": 11.0
|
89
|
+
},
|
90
|
+
{
|
91
|
+
"SourceFile": "b.tif",
|
92
|
+
"FNumber": 9.0
|
93
|
+
},
|
94
|
+
{
|
95
|
+
"SourceFile": "c.bmp",
|
96
|
+
"FNumber": 8.0
|
97
|
+
}]
|
98
|
+
EOS
|
99
|
+
json.gsub!(/^ {8}/, '')
|
100
|
+
mocking_open3('exiftool -J -fnumber a.jpg b.tif c.bmp', json, '')
|
101
|
+
@reader.filenames = %w(a.jpg b.tif c.bmp)
|
102
|
+
@reader.tags = %w(fnumber)
|
103
|
+
res = @reader.read
|
104
|
+
assert_kind_of Array, res
|
105
|
+
assert_equal [11.0, 9.0, 8.0], res.map {|e| e['FNumber']}
|
106
|
+
assert_equal [], @reader.errors
|
107
|
+
end
|
108
|
+
|
109
|
+
test 'successful reading of hierarichal data' do
|
110
|
+
json = <<-EOS
|
111
|
+
[{
|
112
|
+
"SourceFile": "a.jpg",
|
113
|
+
"EXIF": {
|
114
|
+
"FNumber": 7.1
|
115
|
+
},
|
116
|
+
"MakerNotes": {
|
117
|
+
"FNumber": 7.0
|
118
|
+
}
|
119
|
+
}]
|
120
|
+
EOS
|
121
|
+
json.gsub!(/^ {8}/, '')
|
122
|
+
mocking_open3('exiftool -J -g0 -fnumber a.jpg', json, '')
|
123
|
+
@reader.filenames = %w(a.jpg)
|
124
|
+
@reader.tags = %w(fnumber)
|
125
|
+
@reader.group = 0
|
126
|
+
res = @reader.read.first
|
127
|
+
assert_equal 'a.jpg', res.source_file
|
128
|
+
assert_equal 7.1, res.exif.fnumber
|
129
|
+
assert_equal 7.0, res.maker_notes.fnumber
|
130
|
+
assert_equal [], @reader.errors
|
131
|
+
end
|
132
|
+
|
133
|
+
end
|
134
|
+
|
135
|
+
end
|
data/test/test_values.rb
ADDED
@@ -0,0 +1,75 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
require_relative 'helper'
|
3
|
+
require 'date'
|
4
|
+
|
5
|
+
class TestValues < Test::Unit::TestCase
|
6
|
+
|
7
|
+
context 'value access' do
|
8
|
+
|
9
|
+
setup do
|
10
|
+
hash = {'FNumber' => 8, 'Author' => 'janfri'}
|
11
|
+
@values = MultiExiftool::Values.new(hash)
|
12
|
+
end
|
13
|
+
|
14
|
+
test 'original spelling of tag name' do
|
15
|
+
assert_equal 8, @values['FNumber']
|
16
|
+
end
|
17
|
+
|
18
|
+
test 'variant spellings of tag names' do
|
19
|
+
assert_equal 8, @values['fnumber']
|
20
|
+
assert_equal 8, @values['f_number']
|
21
|
+
assert_equal 8, @values['f-number']
|
22
|
+
end
|
23
|
+
|
24
|
+
test 'tag access via methods' do
|
25
|
+
assert_equal 8, @values.fnumber
|
26
|
+
assert_equal 8, @values.f_number
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
|
31
|
+
context 'parsing of values' do
|
32
|
+
|
33
|
+
context 'timestamps' do
|
34
|
+
|
35
|
+
setup do
|
36
|
+
hash = {
|
37
|
+
'TimestampWithoutZone' => '2009:08:25 12:35:42',
|
38
|
+
'TimestampWithPositiveZone' => '2009:08:26 20:22:24+05:00',
|
39
|
+
'TimestampWithNegativeZone' => '2009:08:26 20:22:24-07:00'
|
40
|
+
}
|
41
|
+
@values = MultiExiftool::Values.new(hash)
|
42
|
+
end
|
43
|
+
|
44
|
+
test 'local Time object' do
|
45
|
+
time = Time.local(2009, 8, 25, 12, 35, 42)
|
46
|
+
assert_equal time, @values['TimestampWithoutZone']
|
47
|
+
end
|
48
|
+
|
49
|
+
test 'Time object with given zone' do
|
50
|
+
time = DateTime.new(2009,8,26,20,22,24,'+0500').to_time
|
51
|
+
assert_equal time, @values['TimestampWithPositiveZone']
|
52
|
+
time = DateTime.new(2009,8,26,20,22,24,'-0700').to_time
|
53
|
+
assert_equal time, @values['TimestampWithNegativeZone']
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|
57
|
+
|
58
|
+
context 'other values' do
|
59
|
+
|
60
|
+
setup do
|
61
|
+
hash = {
|
62
|
+
'ShutterSpeed' => '1/200'
|
63
|
+
}
|
64
|
+
@values = MultiExiftool::Values.new(hash)
|
65
|
+
end
|
66
|
+
|
67
|
+
test 'rational values' do
|
68
|
+
assert_equal Rational(1, 200), @values['ShutterSpeed']
|
69
|
+
end
|
70
|
+
|
71
|
+
end
|
72
|
+
|
73
|
+
end
|
74
|
+
|
75
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
require_relative 'helper'
|
3
|
+
|
4
|
+
class TestValuesUsingGroups < Test::Unit::TestCase
|
5
|
+
|
6
|
+
setup do
|
7
|
+
hash = {'EXIF' => {'FNumber' => 8, 'Author' => 'janfri'}}
|
8
|
+
@values = MultiExiftool::Values.new(hash)
|
9
|
+
end
|
10
|
+
|
11
|
+
test 'bracket access' do
|
12
|
+
assert_equal 8, @values['EXIF']['FNumber']
|
13
|
+
assert_equal 'janfri', @values['EXIF']['Author']
|
14
|
+
end
|
15
|
+
|
16
|
+
test 'method access' do
|
17
|
+
assert_equal 8, @values.exif.fnumber
|
18
|
+
assert_equal 'janfri', @values.exif.author
|
19
|
+
end
|
20
|
+
|
21
|
+
test 'mixed access' do
|
22
|
+
assert_equal 8, @values.exif['FNumber']
|
23
|
+
assert_equal 'janfri', @values.exif['Author']
|
24
|
+
assert_equal 8, @values['EXIF'].fnumber
|
25
|
+
assert_equal 'janfri', @values['EXIF'].author
|
26
|
+
end
|
27
|
+
|
28
|
+
test 'block access without block parameter' do
|
29
|
+
$ok = false
|
30
|
+
$block_self = nil
|
31
|
+
res = @values.exif do
|
32
|
+
$ok = true
|
33
|
+
$block_self = self
|
34
|
+
end
|
35
|
+
assert $ok, "Block for exif wasn't executed."
|
36
|
+
assert_equal @values.exif, $block_self
|
37
|
+
assert_equal @values.exif, res
|
38
|
+
@values.iptc do
|
39
|
+
assert false, "This block should not be executed because IPTC isn't aviable."
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
test 'block access with block parameter' do
|
44
|
+
$ok = false
|
45
|
+
$self = self
|
46
|
+
$block_param = nil
|
47
|
+
res = @values.exif do |e|
|
48
|
+
$ok = true
|
49
|
+
$self = self
|
50
|
+
$block_param = e
|
51
|
+
end
|
52
|
+
assert $ok, "Block for exif wasn't executed."
|
53
|
+
assert_equal @values.exif, $block_param
|
54
|
+
assert_equal self, $self
|
55
|
+
assert_equal @values.exif, res
|
56
|
+
@values.iptc do
|
57
|
+
assert false, "This block should not be executed because IPTC isn't aviable."
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|