mini_exiftool 1.0.2 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/Changelog CHANGED
@@ -1,6 +1,10 @@
1
+ Version 1.1.0
2
+ - Escaping filenames in shell commands
3
+ Thanks to Michael Hoy for the hint and implementing a patch which was
4
+ the base for this fix.
1
5
  Version 1.0.2
2
6
  - Fixing warings
3
- thanks to Peter-Hinrich Krogmann for the hint.
7
+ Thanks to Peter-Hinrich Krogmann for the hint.
4
8
  Version 1.0.1
5
9
  - Fixing bug [#22726]
6
10
  Making MiniExiftool::Error public.
@@ -16,6 +16,7 @@ require 'fileutils'
16
16
  require 'tempfile'
17
17
  require 'pstore'
18
18
  require 'set'
19
+ require 'shellwords'
19
20
 
20
21
  # Simple OO access to the Exiftool command-line application.
21
22
  class MiniExiftool
@@ -29,7 +30,7 @@ class MiniExiftool
29
30
  attr_reader :filename
30
31
  attr_accessor :numerical, :composite, :convert_encoding, :errors, :timestamps
31
32
 
32
- VERSION = '1.0.2'
33
+ VERSION = '1.1.0'
33
34
 
34
35
  # +opts+ support at the moment
35
36
  # * <code>:numerical</code> for numerical values, default is +false+
@@ -80,7 +81,7 @@ class MiniExiftool
80
81
  opt_params << (@numerical ? '-n ' : '')
81
82
  opt_params << (@composite ? '' : '-e ')
82
83
  opt_params << (@convert_encoding ? '-L ' : '')
83
- cmd = %Q(#@@cmd -q -q -s -t #{opt_params} #{@@sep_op} "#{filename}")
84
+ cmd = %Q(#@@cmd -q -q -s -t #{opt_params} #{@@sep_op} #{Shellwords.escape(filename)})
84
85
  if run(cmd)
85
86
  parse_output
86
87
  else
@@ -156,7 +157,7 @@ class MiniExiftool
156
157
  opt_params = ''
157
158
  opt_params << (arr_val.detect {|x| x.kind_of?(Numeric)} ? '-n ' : '')
158
159
  opt_params << (@convert_encoding ? '-L ' : '')
159
- cmd = %Q(#@@cmd -q -P -overwrite_original #{opt_params} #{tag_params} "#{temp_filename}")
160
+ cmd = %Q(#@@cmd -q -P -overwrite_original #{opt_params} #{tag_params} #{temp_filename})
160
161
  result = run(cmd)
161
162
  unless result
162
163
  all_ok = false
@@ -27,14 +27,6 @@ class TestClassMethods < TestCase
27
27
  rescue MiniExiftool::Error => e
28
28
  assert_match /File 'not_existing_file' does not exist/, e.message
29
29
  end
30
- assert_raises MiniExiftool::Error do
31
- MiniExiftool.new __FILE__ # file type wich Exiftool can not handle
32
- end
33
- begin
34
- MiniExiftool.new __FILE__ # file type wich Exiftool can not handle
35
- rescue MiniExiftool::Error => e
36
- assert_match /Error: Unknown (?:image|file) type/, e.message
37
- end
38
30
  end
39
31
 
40
32
  def test_command
@@ -76,14 +76,4 @@ class TestDumping < TestCase
76
76
  assert_equal true, MiniExiftool.from_yaml(numerical.to_yaml).numerical
77
77
  end
78
78
 
79
- def test_heuristics_for_restoring_timestamps
80
- standard = @mini_exiftool.to_hash
81
- timestamps = MiniExiftool.new(@filename_test, :timestamps => DateTime).to_hash
82
- assert_equal Time, MiniExiftool.from_hash(standard).timestamps
83
- assert_equal DateTime, MiniExiftool.from_hash(timestamps).timestamps
84
- # Doesn't work yet.
85
- # assert_equal Time, MiniExiftool.from_yaml(standard.to_yaml).timestamps
86
- # assert_equal DateTime, MiniExiftool.from_yaml(timestamps.to_yaml).timestamps
87
- end
88
-
89
79
  end
@@ -0,0 +1,25 @@
1
+ require 'helpers_for_test'
2
+
3
+ class TestEscapeFilename < TestCase
4
+
5
+ def setup
6
+ @temp_file = Tempfile.new('test')
7
+ @temp_file.close
8
+ @temp_filename = @temp_file.path
9
+ @org_filename = File.dirname(__FILE__) + '/data/test 36"Bench.jpg'
10
+ FileUtils.cp(@org_filename, @temp_filename)
11
+ @mini_exiftool = MiniExiftool.new @temp_filename
12
+ end
13
+
14
+ def test_access
15
+ assert_equal '36" Bench', @mini_exiftool['description']
16
+ end
17
+
18
+ def test_save
19
+ desc = 'another bench'
20
+ @mini_exiftool.description = desc
21
+ assert @mini_exiftool.save
22
+ assert_equal desc, @mini_exiftool.description
23
+ end
24
+
25
+ end
@@ -25,7 +25,7 @@ class TestRead < TestCase
25
25
  assert_kind_of Float, @mini_exiftool['MaxApertureValue']
26
26
  assert_kind_of String, @mini_exiftool.flash
27
27
  assert_kind_of Fixnum, @mini_exiftool['ExposureCompensation']
28
- assert_kind_of String, @mini_exiftool['SubjectLocation']
28
+ assert_kind_of String, (@mini_exiftool['SubjectLocation'] || @mini_exiftool['SubjectArea'])
29
29
  assert_kind_of Array, @mini_exiftool['Keywords']
30
30
  assert_kind_of String, @mini_exiftool['SupplementalCategories']
31
31
  assert_kind_of Array, @mini_exiftool['SupplementalCategories'].to_a
@@ -22,7 +22,7 @@ class TestReadNumerical < TestCase
22
22
  assert_kind_of Fixnum, @mini_exiftool_num.flash
23
23
  assert_kind_of String, @mini_exiftool_num.exif_version
24
24
  assert_kind_of Fixnum, @mini_exiftool_num['ExposureCompensation']
25
- assert_kind_of String, @mini_exiftool_num['SubjectLocation']
25
+ assert_kind_of String, (@mini_exiftool_num['SubjectLocation'] || @mini_exiftool_num['SubjectArea'])
26
26
  assert_kind_of Array, @mini_exiftool_num['Keywords']
27
27
  assert_kind_of String, @mini_exiftool_num['SupplementalCategories']
28
28
  assert_kind_of Array, @mini_exiftool_num['SupplementalCategories'].to_a
@@ -32,7 +32,7 @@ class TestSave < TestCase
32
32
  result = @mini_exiftool.save
33
33
  assert_equal false, result
34
34
  assert_equal 1, @mini_exiftool.errors.size
35
- assert_equal("Can't convert IFD0:Orientation (not in PrintConv)",
35
+ assert_match(/Can't convert IFD0:Orientation \(not in PrintConv\)/,
36
36
  @mini_exiftool.errors['Orientation'])
37
37
  assert @mini_exiftool.changed?
38
38
  assert @mini_exiftool.changed_tags.include?('Orientation')
metadata CHANGED
@@ -5,9 +5,9 @@ version: !ruby/object:Gem::Version
5
5
  prerelease: false
6
6
  segments:
7
7
  - 1
8
+ - 1
8
9
  - 0
9
- - 2
10
- version: 1.0.2
10
+ version: 1.1.0
11
11
  platform: ruby
12
12
  authors:
13
13
  - Jan Friedrich
@@ -15,7 +15,7 @@ autorequire:
15
15
  bindir: bin
16
16
  cert_chain: []
17
17
 
18
- date: 2010-12-30 00:00:00 +01:00
18
+ date: 2011-02-10 00:00:00 +01:00
19
19
  default_executable:
20
20
  dependencies: []
21
21
 
@@ -32,25 +32,25 @@ files:
32
32
  - examples/external_photo.rb
33
33
  - examples/print_portraits.rb
34
34
  - examples/shift_time.rb
35
- - lib/mini_exiftool.new.rb
36
35
  - lib/mini_exiftool.rb
37
36
  - setup.rb
38
37
  - test/helpers_for_test.rb
39
38
  - test/test_class_methods.rb
40
39
  - test/test_composite.rb
41
40
  - test/test_dumping.rb
41
+ - test/test_escape_filename.rb
42
42
  - test/test_read.rb
43
43
  - test/test_read_numerical.rb
44
44
  - test/test_save.rb
45
45
  - test/test_special.rb
46
46
  - test/test_special_dates.rb
47
47
  - test/test_write.rb
48
- - x.rb
49
48
  - Rakefile
50
49
  - COPYING
51
50
  - Changelog
52
51
  - test/data/Canon.jpg
53
52
  - test/data/INFORMATION
53
+ - test/data/test 36"Bench.jpg
54
54
  - test/data/test.jpg
55
55
  - test/data/test_special_dates.jpg
56
56
  - README
@@ -96,6 +96,7 @@ test_files:
96
96
  - test/test_class_methods.rb
97
97
  - test/test_composite.rb
98
98
  - test/test_dumping.rb
99
+ - test/test_escape_filename.rb
99
100
  - test/test_read.rb
100
101
  - test/test_read_numerical.rb
101
102
  - test/test_save.rb
@@ -1,445 +0,0 @@
1
- #
2
- # MiniExiftool
3
- #
4
- # This library is wrapper for the Exiftool command-line
5
- # application (http://www.sno.phy.queensu.ca/~phil/exiftool/)
6
- # written by Phil Harvey.
7
- # Read and write access is done in a clean OO manner.
8
- #
9
- # Author: Jan Friedrich
10
- # Copyright (c) 2007, 2008 by Jan Friedrich
11
- # Licensed under the GNU LESSER GENERAL PUBLIC LICENSE,
12
- # Version 2.1, February 1999
13
- #
14
-
15
- require 'fileutils'
16
- require 'tempfile'
17
- require 'pstore'
18
- require 'set'
19
-
20
- # Simple OO access to the Exiftool command-line application.
21
- class MiniExiftool
22
-
23
- # Name of the Exiftool command-line application
24
- @@cmd = 'exiftool'
25
-
26
- # Hash of the standard options used when call MiniExiftool.new
27
- @@opts = { :numerical => false, :composite => true, :convert_encoding => false, :timestamps => Time }
28
-
29
- attr_reader :filename
30
- attr_accessor :numerical, :composite, :convert_encoding, :errors, :timestamps
31
-
32
- VERSION = '1.0.1'
33
-
34
- # +opts+ support at the moment
35
- # * <code>:numerical</code> for numerical values, default is +false+
36
- # * <code>:composite</code> for including composite tags while loading,
37
- # default is +true+
38
- # * <code>:convert_encoding</code> convert encoding (See -L-option of
39
- # the exiftool command-line application, default is +false+
40
- # * <code>:timestamps</code> generating DateTime objects instead of
41
- # Time objects if set to <code>DateTime</code>, default is +Time+
42
- #
43
- # <b>ATTENTION:</b> Time objects are created using <code>Time.local</code>
44
- # therefore they use <em>your local timezone</em>, DateTime objects instead
45
- # are created <em>without timezone</em>!
46
- def initialize filename=nil, opts={}
47
- opts = @@opts.merge opts
48
- @numerical = opts[:numerical]
49
- @composite = opts[:composite]
50
- @convert_encoding = opts[:convert_encoding]
51
- @timestamps = opts[:timestamps]
52
- @values = TagHash.new
53
- @tag_names = TagHash.new
54
- @changed_values = TagHash.new
55
- @errors = TagHash.new
56
- load filename unless filename.nil?
57
- end
58
-
59
- def initialize_from_hash hash # :nodoc:
60
- hash.each_pair do |tag,value|
61
- set_value tag, value
62
- end
63
- set_attributes_by_heuristic
64
- self
65
- end
66
-
67
- # Load the tags of filename.
68
- def load filename
69
- unless filename && File.exist?(filename)
70
- raise MiniExiftool::Error.new("File '#{filename}' does not exist.")
71
- end
72
- if File.directory?(filename)
73
- raise MiniExiftool::Error.new("'#{filename}' is a directory.")
74
- end
75
- @filename = filename
76
- @values.clear
77
- @tag_names.clear
78
- @changed_values.clear
79
- opt_params = ''
80
- opt_params << (@numerical ? '-n ' : '')
81
- opt_params << (@composite ? '' : '-e ')
82
- opt_params << (@convert_encoding ? '-L ' : '')
83
- cmd = %Q(#@@cmd -q -q -s -t #{opt_params} #{@@sep_op} "#{filename}")
84
- if run(cmd)
85
- parse_output
86
- else
87
- raise MiniExiftool::Error.new(@error_text)
88
- end
89
- self
90
- end
91
-
92
- # Reload the tags of an already readed file.
93
- def reload
94
- load @filename
95
- end
96
-
97
- # Returns the value of a tag.
98
- def [] tag
99
- @changed_values[tag] || @values[tag]
100
- end
101
-
102
- # Set the value of a tag.
103
- def []=(tag, val)
104
- @changed_values[tag] = val
105
- end
106
-
107
- # Returns true if any tag value is changed or if the value of a
108
- # given tag is changed.
109
- def changed? tag=false
110
- if tag
111
- @changed_values.include? tag
112
- else
113
- !@changed_values.empty?
114
- end
115
- end
116
-
117
- # Revert all changes or the change of a given tag.
118
- def revert tag=nil
119
- if tag
120
- val = @changed_values.delete(tag)
121
- res = val != nil
122
- else
123
- res = @changed_values.size > 0
124
- @changed_values.clear
125
- end
126
- res
127
- end
128
-
129
- # Returns an array of the tags (original tag names) of the readed file.
130
- def tags
131
- @values.keys.map { |key| @tag_names[key] }
132
- end
133
-
134
- # Returns an array of all changed tags.
135
- def changed_tags
136
- @changed_values.keys.map { |key| MiniExiftool.original_tag(key) }
137
- end
138
-
139
- # Save the changes to the file.
140
- def save
141
- return false if @changed_values.empty?
142
- @errors.clear
143
- temp_file = Tempfile.new('mini_exiftool')
144
- temp_file.close
145
- temp_filename = temp_file.path
146
- FileUtils.cp filename, temp_filename
147
- all_ok = true
148
- @changed_values.each do |tag, val|
149
- original_tag = MiniExiftool.original_tag(tag)
150
- arr_val = val.kind_of?(Array) ? val : [val]
151
- arr_val.map! {|e| convert e}
152
- tag_params = ''
153
- arr_val.each do |v|
154
- tag_params << %Q(-#{original_tag}="#{v}" )
155
- end
156
- opt_params = ''
157
- opt_params << (arr_val.detect {|x| x.kind_of?(Numeric)} ? '-n ' : '')
158
- opt_params << (@convert_encoding ? '-L ' : '')
159
- cmd = %Q(#@@cmd -q -P -overwrite_original #{opt_params} #{tag_params} "#{temp_filename}")
160
- result = run(cmd)
161
- unless result
162
- all_ok = false
163
- @errors[tag] = @error_text.gsub(/Nothing to do.\n\z/, '').chomp
164
- end
165
- end
166
- if all_ok
167
- FileUtils.cp temp_filename, filename
168
- reload
169
- end
170
- temp_file.delete
171
- all_ok
172
- end
173
-
174
- # Returns a hash of the original loaded values of the MiniExiftool
175
- # instance.
176
- def to_hash
177
- result = {}
178
- @values.each do |k,v|
179
- result[@tag_names[k]] = v
180
- end
181
- result
182
- end
183
-
184
- # Returns a YAML representation of the original loaded values of the
185
- # MiniExiftool instance.
186
- def to_yaml
187
- to_hash.to_yaml
188
- end
189
-
190
- # Create a MiniExiftool instance from a hash
191
- def self.from_hash hash
192
- instance = MiniExiftool.new
193
- instance.initialize_from_hash hash
194
- instance
195
- end
196
-
197
- # Create a MiniExiftool instance from YAML data created with
198
- # MiniExiftool#to_yaml
199
- def self.from_yaml yaml
200
- MiniExiftool.from_hash YAML.load(yaml)
201
- end
202
-
203
- # Returns the command name of the called Exiftool application.
204
- def self.command
205
- @@cmd
206
- end
207
-
208
- # Setting the command name of the called Exiftool application.
209
- def self.command= cmd
210
- @@cmd = cmd
211
- end
212
-
213
- # Returns the options hash.
214
- def self.opts
215
- @@opts
216
- end
217
-
218
- # Returns a set of all known tags of Exiftool.
219
- def self.all_tags
220
- unless defined? @@all_tags
221
- @@all_tags = pstore_get :all_tags
222
- end
223
- @@all_tags
224
- end
225
-
226
- # Returns a set of all possible writable tags of Exiftool.
227
- def self.writable_tags
228
- unless defined? @@writable_tags
229
- @@writable_tags = pstore_get :writable_tags
230
- end
231
- @@writable_tags
232
- end
233
-
234
- # Returns the original Exiftool name of the given tag
235
- def self.original_tag tag
236
- unless defined? @@all_tags_map
237
- @@all_tags_map = pstore_get :all_tags_map
238
- end
239
- @@all_tags_map[tag]
240
- end
241
-
242
- # Returns the version of the Exiftool command-line application.
243
- def self.exiftool_version
244
- output = `#{MiniExiftool.command} -ver 2>&1`
245
- unless $?.exitstatus == 0
246
- raise MiniExiftool::Error.new("Command '#{MiniExiftool.command}' not found")
247
- end
248
- output.chomp!
249
- end
250
-
251
- def self.unify tag
252
- tag.gsub(/[-_]/,'').downcase
253
- end
254
-
255
- # Exception class
256
- class MiniExiftool::Error < StandardError; end
257
-
258
- ############################################################################
259
- private
260
- ############################################################################
261
-
262
- @@error_file = Tempfile.new 'errors'
263
- @@error_file.close
264
-
265
- if Float(exiftool_version) < 7.41
266
- @@separator = ', '
267
- @@sep_op = ''
268
- else
269
- @@separator = '@@'
270
- @@sep_op = '-sep @@'
271
- end
272
-
273
- def run cmd
274
- if $DEBUG
275
- $stderr.puts cmd
276
- end
277
- @output = `#{cmd} 2>#{@@error_file.path}`
278
- @status = $?
279
- unless @status.exitstatus == 0
280
- @error_text = File.readlines(@@error_file.path).join
281
- return false
282
- else
283
- @error_text = ''
284
- return true
285
- end
286
- end
287
-
288
- def convert val
289
- case val
290
- when Time
291
- val = val.strftime('%Y:%m:%d %H:%M:%S')
292
- end
293
- val
294
- end
295
-
296
- def method_missing symbol, *args
297
- tag_name = symbol.id2name
298
- if tag_name.sub!(/=$/, '')
299
- self[tag_name] = args.first
300
- else
301
- self[tag_name]
302
- end
303
- end
304
-
305
- def parse_output
306
- @output.each_line do |line|
307
- tag, value = parse_line line
308
- set_value tag, value
309
- end
310
- end
311
-
312
- def parse_line line
313
- if line =~ /^([^\t]+)\t(.*)$/
314
- tag, value = $1, $2
315
- case value
316
- when /^\d{4}:\d\d:\d\d \d\d:\d\d:\d\d$/
317
- arr = value.split(/[: ]/) #Fixed by kro44 - add parentheses
318
- arr.map! {|elem| elem.to_i}
319
- begin
320
- if @timestamps == Time
321
- value = Time.local(*arr) #Fixed by kro44 - add parentheses
322
- elsif @timestamps == DateTime
323
- value = DateTime.strptime(value,'%Y:%m:%d %H:%M:%S')
324
- else
325
- raise MiniExiftool::Error.new("Value #@timestamps not allowed for option timestamps.")
326
- end
327
- rescue ArgumentError
328
- value = false
329
- end
330
- when /^\d+\.\d+$/
331
- value = value.to_f
332
- when /^0+[1-9]+$/
333
- # nothing => String
334
- when /^-?\d+$/
335
- value = value.to_i
336
- when /^[\d ]+$/
337
- # nothing => String
338
- when /#{@@separator}/
339
- value = value.split @@separator
340
- end
341
- else
342
- raise MiniExiftool::Error.new("Malformed line #{line.inspect} of exiftool output.")
343
- end
344
- unless value.respond_to?('to_a')
345
- class << value
346
- def to_a
347
- [self]
348
- end
349
- end
350
- end
351
- return [tag, value]
352
- end
353
-
354
- def set_value tag, value
355
- @tag_names[tag] = tag
356
- @values[tag] = value
357
- end
358
-
359
- def set_attributes_by_heuristic
360
- self.composite = tags.include?('ImageSize') ? true : false
361
- self.numerical = self.file_size.kind_of?(Integer) ? true : false
362
- # TODO: Is there a heuristic to determine @convert_encoding?
363
- self.timestamps = self.FileModifyDate.kind_of?(DateTime) ? DateTime : Time
364
- end
365
-
366
- def temp_filename
367
- unless @temp_filename
368
- temp_file = Tempfile.new('mini-exiftool')
369
- temp_file.close
370
- FileUtils.cp(@filename, temp_file.path)
371
- @temp_filename = temp_file.path
372
- end
373
- @temp_filename
374
- end
375
-
376
- def self.pstore_get attribute
377
- load_or_create_pstore unless defined? @@pstore
378
- result = nil
379
- @@pstore.transaction(true) do |ps|
380
- result = ps[attribute]
381
- end
382
- result
383
- end
384
-
385
- def self.load_or_create_pstore
386
- # This will hopefully work on *NIX and Windows systems
387
- home = ENV['HOME'] || ENV['HOMEDRIVE'] + ENV['HOMEPATH'] || ENV['USERPROFILE']
388
- subdir = RUBY_PLATFORM =~ /win/i ? '_mini_exiftool' : '.mini_exiftool'
389
- FileUtils.mkdir_p(File.join(home, subdir))
390
- filename = File.join(home, subdir, 'exiftool_tags_' << exiftool_version.gsub('.', '_') << '.pstore')
391
- @@pstore = PStore.new filename
392
- if !File.exist?(filename) || File.size(filename) == 0
393
- @@pstore.transaction do |ps|
394
- ps[:all_tags] = all_tags = determine_tags('list')
395
- ps[:writable_tags] = determine_tags('listw')
396
- map = {}
397
- all_tags.each { |k| map[unify(k)] = k }
398
- ps[:all_tags_map] = map
399
- end
400
- end
401
- end
402
-
403
- def self.determine_tags arg
404
- output = `#{@@cmd} -#{arg}`
405
- lines = output.split(/\n/) #Fixed by kro44 - add parentheses
406
- tags = Set.new
407
- lines.each do |line|
408
- next unless line =~ /^\s/
409
- tags |= line.chomp.split
410
- end
411
- tags
412
- end
413
-
414
-
415
- # Hash with indifferent access:
416
- # DateTimeOriginal == datetimeoriginal == date_time_original
417
- class TagHash < Hash # :nodoc:
418
- def[] k
419
- super(unify(k))
420
- end
421
- def []= k, v
422
- super(unify(k), v)
423
- end
424
- def delete k
425
- super(unify(k))
426
- end
427
-
428
- def unify tag
429
- MiniExiftool.unify tag
430
- end
431
- end
432
-
433
- end
434
-
435
- # Add to_a to Numerical if it's not yet defined
436
- unless Numeric.instance_methods.include? 'to_a'
437
- class Numeric
438
- def to_a
439
- [self]
440
- end
441
- end
442
- end
443
-
444
- # Test if we can run the Exiftool command
445
- MiniExiftool.exiftool_version
data/x.rb DELETED
@@ -1,3 +0,0 @@
1
- require 'mini_exiftool'
2
-
3
- puts MiniExiftool.exiftool_version