multi_exiftool 0.0.1 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|