exiftoolr 0.0.6 → 0.0.7

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,9 @@
1
+ language: ruby
2
+ script: bundle exec rake
3
+ before_install:
4
+ - sudo apt-get update
5
+ - sudo apt-get install libimage-exiftool-perl
6
+ rvm:
7
+ - 1.8.7
8
+ - 1.9.3
9
+ - rbx-18mode
data/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # Ruby wrapper for ExifTool
2
2
 
3
+ [![Build Status](https://secure.travis-ci.org/mceachen/exiftoolr.png?branch=master)](http://travis-ci.org/mceachen/exiftoolr)
4
+
3
5
  This gem is the simplest thing that could possibly work that
4
6
  reads the output of [exiftool](http://www.sno.phy.queensu.ca/~phil/exiftool)
5
7
  and renders it into a ruby hash, with correctly typed values and symbolized keys.
@@ -9,7 +11,7 @@ and renders it into a ruby hash, with correctly typed values and symbolized keys
9
11
  * GPS latitude and longitude are rendered as signed floats,
10
12
  where north and east are positive, and west and south are negative.
11
13
  * Values like shutter speed and exposure time are rendered as Rationals,
12
- which lets the caller show them as fractions (1/250) or as comparable.
14
+ which lets the caller show them as fractions (1/250) or as comparable numeric instances.
13
15
  * String values like "interop" and "serial number" are kept as strings
14
16
  (which preserves zero prefixes)
15
17
 
@@ -70,10 +72,16 @@ Exiftoolr.new("Gemfile").errors?
70
72
 
71
73
  ## Change history
72
74
 
73
- ### 0.0.4
75
+ ### 0.0.7
74
76
 
75
- Added support for multiple file fetching (which is *much* faster for large directories)
77
+ * Added warning values for EXIF headers that are corrupt
78
+ * Made initialize gracefully accept an empty array, or an array of Pathname instances
79
+ * Added support for ruby 1.9 and exiftool v 8.15 (Ubuntu Natty)
76
80
 
77
81
  ### 0.0.5
78
82
 
79
83
  Fixed homepage URL in gemspec
84
+
85
+ ### 0.0.4
86
+
87
+ Added support for multiple file fetching (which is *much* faster for large directories)
@@ -22,5 +22,4 @@ Gem::Specification.new do |s|
22
22
 
23
23
  s.add_dependency "json"
24
24
  s.add_development_dependency "rake"
25
- s.add_development_dependency "rspec"
26
25
  end
@@ -5,12 +5,16 @@ require 'json'
5
5
  require 'shellwords'
6
6
 
7
7
  class Exiftoolr
8
- class NoSuchFile < StandardError; end
9
- class NotAFile < StandardError; end
10
- class ExiftoolNotInstalled < StandardError; end
8
+ class NoSuchFile < StandardError ; end
9
+ class NotAFile < StandardError ; end
10
+ class ExiftoolNotInstalled < StandardError ; end
11
11
 
12
12
  def self.exiftool_installed?
13
- `exiftool -ver 2> /dev/null`.to_f > 0
13
+ exiftool_version > 0
14
+ end
15
+
16
+ def self.exiftool_version
17
+ @@exiftool_version ||= `exiftool -ver 2> /dev/null`.to_f
14
18
  end
15
19
 
16
20
  def self.expand_path(filename)
@@ -20,15 +24,19 @@ class Exiftoolr
20
24
  end
21
25
 
22
26
  def initialize(filenames, exiftool_opts = "")
23
- escaped_filenames = filenames.to_a.collect do |f|
24
- Shellwords.escape(self.class.expand_path(f))
25
- end.join(" ")
26
- json = `exiftool #{exiftool_opts} -j −coordFormat "%.8f" -dateFormat "%Y-%m-%d %H:%M:%S" #{escaped_filenames} 2> /dev/null`
27
- raise ExiftoolNotInstalled if json == ""
28
- @file2result = { }
29
- JSON.parse(json).each do |raw|
30
- result = Result.new(raw)
31
- @file2result[result.source_file] = result
27
+ @file2result = {}
28
+ filenames = [filenames] if filenames.is_a?(String)
29
+ unless filenames.empty?
30
+ escaped_filenames = filenames.collect do |f|
31
+ Shellwords.escape(self.class.expand_path(f.to_s))
32
+ end.join(" ")
33
+ cmd = "exiftool #{exiftool_opts} -j -coordFormat \"%.8f\" -dateFormat \"%Y-%m-%d %H:%M:%S\" #{escaped_filenames} 2> /dev/null"
34
+ json = `#{cmd}`
35
+ raise ExiftoolNotInstalled if json == ""
36
+ JSON.parse(json).each do |raw|
37
+ result = Result.new(raw)
38
+ @file2result[result.source_file] = result
39
+ end
32
40
  end
33
41
  end
34
42
 
@@ -37,7 +45,7 @@ class Exiftoolr
37
45
  end
38
46
 
39
47
  def files_with_results
40
- @file2result.values.collect{|r|r.source_file unless r.errors?}.compact
48
+ @file2result.values.collect { |r| r.source_file unless r.errors? }.compact
41
49
  end
42
50
 
43
51
  def to_hash
@@ -6,7 +6,7 @@ class Exiftoolr
6
6
  attr_reader :to_hash, :to_display_hash, :symbol_display_hash
7
7
 
8
8
  WORD_BOUNDARY_RES = [/([A-Z\d]+)([A-Z][a-z])/, /([a-z\d])([A-Z])/]
9
- FRACTION_RE = /\d+\/\d+/
9
+ FRACTION_RE = /^(\d+)\/(\d+)$/
10
10
 
11
11
  def initialize(raw_hash)
12
12
  @raw_hash = raw_hash
@@ -14,20 +14,29 @@ class Exiftoolr
14
14
  @to_display_hash = { }
15
15
  @symbol_display_hash = { }
16
16
 
17
- @raw_hash.each do |k, v|
17
+ @raw_hash.each do |k, raw_v|
18
18
  display_key = WORD_BOUNDARY_RES.inject(k) { |key, regex| key.gsub(regex, '\1 \2') }
19
19
  sym_key = display_key.downcase.gsub(' ', '_').to_sym
20
- if sym_key == :gps_latitude || sym_key == :gps_longitude
21
- value, direction = v.split(" ")
22
- v = value.to_f
23
- v *= -1 if direction == 'S' || direction == 'W'
24
- elsif display_key =~ /\bdate\b/i
25
- v = Time.parse(v)
26
- elsif v =~ FRACTION_RE
27
- v = Rational(*v.split('/').collect { |ea| ea.to_i })
20
+ begin
21
+ if sym_key == :gps_latitude || sym_key == :gps_longitude
22
+ value, direction = raw_v.split(" ")
23
+ v = value.to_f
24
+ v *= -1 if direction == 'S' || direction == 'W'
25
+ elsif raw_v.is_a?(String)
26
+ if display_key =~ /\bdate\b/i
27
+ v = Time.parse(raw_v)
28
+ else
29
+ scan = raw_v.scan(FRACTION_RE).first
30
+ unless scan.nil?
31
+ v = Rational(*scan.collect { |ea| ea.to_i })
32
+ end
33
+ end
34
+ end
35
+ rescue StandardError => e
36
+ v = "Warning: Parsing '#{raw_v}' for attribute '#{k}' raised #{e.message}"
28
37
  end
29
- @to_hash[sym_key] = v
30
- @to_display_hash[display_key] = v
38
+ @to_hash[sym_key] = v || raw_v
39
+ @to_display_hash[display_key] = v || raw_v
31
40
  @symbol_display_hash[sym_key] = display_key
32
41
  end
33
42
  end
@@ -1,3 +1,3 @@
1
1
  class Exiftoolr
2
- VERSION = "0.0.6"
2
+ VERSION = "0.0.7"
3
3
  end
@@ -166,9 +166,7 @@
166
166
  :aperture: 9.0
167
167
  :easy_mode: Manual
168
168
  :nd_filter: n/a
169
- :exposure_level_increments: !ruby/object:Rational
170
- denominator: 3
171
- numerator: 1
169
+ :exposure_level_increments: "1/3 Stop"
172
170
  :auto_exposure_bracketing: "Off"
173
171
  :af_area_height: 78
174
172
  :flash_firing: Fires
@@ -10,9 +10,6 @@
10
10
  :wb_shift_ab: 0
11
11
  :lens35efl: "17.0 - 40.0 mm (35 mm equivalent: 26.6 - 62.5 mm)"
12
12
  :thumbnail_image_valid_area: 0 159 7 112
13
- :"profile_description_ml-zh-cn": !binary |
14
- 55u45py6IFJHQiDmj4/ov7Dmlofku7Y=
15
-
16
13
  :current_iptc_digest: b443520a10119da99c2550175e6d0efb
17
14
  :model: Canon EOS 20D
18
15
  :af_point_selection_method: Normal
@@ -21,16 +18,12 @@
21
18
  :user_comment: ""
22
19
  :color_space: sRGB
23
20
  :focus_range: Not Known
24
- :"profile_description_ml-ja-jp": !binary |
25
- 44Kr44Oh44OpIFJHQiDjg5fjg63jg5XjgqHjgqTjg6s=
26
-
27
21
  :zoom_target_width: 0
28
22
  :device_model: ""
29
23
  :flash_guide_number: 0
30
24
  :focal_plane_x_size: 23.04 mm
31
25
  :saturation: Normal
32
26
  :quality: Fine
33
- :"profile_description_ml-it-it": Profilo RGB Fotocamera
34
27
  :focal_type: Zoom
35
28
  :iso: 400
36
29
  :sequence_number: 0
@@ -47,9 +40,6 @@
47
40
  :sensor_left_border: 84
48
41
  :sensor_red_level: 0
49
42
  :wb_rggb_levels_custom1: 1096 1023 1023 2793
50
- :"profile_description_ml-ko-kr": !binary |
51
- 7Lm066mU6528IFJHQiDtlITroZztjIzsnbw=
52
-
53
43
  :photo_effect: "Off"
54
44
  :af_area_x_positions: 8 -555 571 -931 8 947 -555 571 8
55
45
  :profile_version: 2.2.0
@@ -105,7 +95,6 @@
105
95
  :x_resolution: 72
106
96
  :cmm_flags: Not Embedded, Independent
107
97
  :focus_mode: One-shot AF
108
- :"profile_description_ml-fi-fi": Kameran RGB-profiili
109
98
  :blue_matrix_column: 0.15662 0.08336 0.71953
110
99
  :canon_image_size: Large
111
100
  :original_decision_data_offset: 3968887
@@ -129,9 +118,7 @@
129
118
  :manual_flash_output: n/a
130
119
  :y_resolution: 72
131
120
  :white_balance: Auto
132
- :flash_exposure_comp: !ruby/object:Rational
133
- denominator: 3
134
- numerator: 2
121
+ :flash_exposure_comp: "+2/3"
135
122
  :superimposed_display: "On"
136
123
  :date_time_original: 2005-11-26 14:20:34 -08:00
137
124
  :wb_rggb_levels_cloudy: 2087 1023 1023 1459
@@ -148,7 +135,6 @@
148
135
  :filter_effect: None
149
136
  :sensor_right_border: 3587
150
137
  :canon_exposure_mode: Depth-of-field AE
151
- :"profile_description_ml-es-es": "Perfil RGB para C\xC3\xA1mara"
152
138
  :profile_description_ml: Camera RGB Profile
153
139
  :resolution_unit: inches
154
140
  :region_area_w:
@@ -185,7 +171,6 @@
185
171
  :file_permissions: rw-r--r--
186
172
  :lens: 17.0 - 40.0 mm
187
173
  :add_original_decision_data: "On"
188
- :"profile_description_ml-nl-nl": RGB-profiel Camera
189
174
  :profile_class: Input Device Profile
190
175
  :flash: Off, Did not fire
191
176
  :digital_gain: 0
@@ -211,9 +196,7 @@
211
196
  :set_function_when_shooting: Change quality
212
197
  :green_trc: (Binary data 14 bytes)
213
198
  :vrd_offset: 0
214
- :exposure_level_increments: !ruby/object:Rational
215
- denominator: 3
216
- numerator: 1
199
+ :exposure_level_increments: "1/3 Stop"
217
200
  :wb_bracket_value_gm: 0
218
201
  :region_area_h:
219
202
  - 0.119007
@@ -238,8 +221,7 @@
238
221
  :thumbnail_offset: 4036
239
222
  :sharpness_frequency: n/a
240
223
  :wb_shift_gm: 0
241
- :"profile_description_ml-da-dk": RGB-beskrivelse til Kamera
242
- :region_name:
224
+ :region_name:
243
225
  - Matthew McEachen
244
226
  - Karen McEachen
245
227
  - Ruth McEachen
@@ -254,7 +236,6 @@
254
236
  :bulb_duration: 0
255
237
  :color_temp_fluorescent: 3961
256
238
  :bracket_shot_number: 0
257
- :"profile_description_ml-pt-br": "Perfil RGB de C\xC3\xA2mera"
258
239
  :picture_style: None
259
240
  :bits_per_sample: 8
260
241
  :region_type:
@@ -267,9 +248,6 @@
267
248
  :sharpness: 0
268
249
  :file_number: 326-2647
269
250
  :modify_date: 2012-02-15 22:58:07 -08:00
270
- :"profile_description_ml-zh-tw": !binary |
271
- 5pW45L2N55u45qmfIFJHQiDoibLlvanmj4/ov7A=
272
-
273
251
  :self_timer: "Off"
274
252
  :encoding_process: Baseline DCT, Huffman coding
275
253
  :color_temp_auto: 4599
@@ -295,12 +273,10 @@
295
273
  :long_focal: 40 mm
296
274
  :file_modify_date: 2012-02-19 21:49:48 -08:00
297
275
  :flash_sync_speed_av: Auto
298
- :"profile_description_ml-sv-se": "RGB-profil f\xC3\xB6r Kamera"
299
276
  :profile_connection_space: "XYZ "
300
277
  :wb_rggb_levels_daylight: 1933 1023 1023 1607
301
278
  :black_mask_right_border: 0
302
279
  :camera_type: EOS Mid-range
303
- :"profile_description_ml-de-de": "RGB-Profil f\xC3\xBCr Kameras"
304
280
  :record_mode: JPEG
305
281
  :interop_index: R98 - DCF basic file (sRGB)
306
282
  :profile_date_time: 2003-07-01 00:00:00 -07:00
@@ -308,12 +284,10 @@
308
284
  :focal_plane_x_resolution: 3959.322034
309
285
  :flashpix_version: "0100"
310
286
  :canon_image_height: 2336
311
- :"profile_description_ml-no-no": RGB-kameraprofil
312
287
  :profile_description: Camera RGB Profile
313
288
  :wb_rggb_levels_tungsten: 1382 1023 1023 2574
314
289
  :black_mask_top_border: 0
315
290
  :exposure_program: Not Defined
316
- :"profile_description_ml-fr-fu": "Profil RVB de l\xE2\x80\x99appareil-photo"
317
291
  :blue_balance: 1.683744
318
292
  :ettlii: Evaluative
319
293
  :image_width: 3504
@@ -18,6 +18,10 @@ class TestExiftoolr < Test::Unit::TestCase
18
18
  end
19
19
  end
20
20
 
21
+ def test_no_files
22
+ assert !Exiftoolr.new([]).errors?
23
+ end
24
+
21
25
  def test_invalid_exif
22
26
  assert Exiftoolr.new("Gemfile").errors?
23
27
  end
@@ -41,11 +45,27 @@ class TestExiftoolr < Test::Unit::TestCase
41
45
  File.open(yaml_file, 'w') { |out| YAML.dump(exif, out) } if DUMP_RESULTS
42
46
  e = File.open(yaml_file) { |f| YAML::load(f) }
43
47
  exif.keys.each do |k|
44
- next if [:file_modify_date, :directory, :source_file].include? k
45
- assert_equal e[k], exif[k], "Key '#{k}' was incorrect for #{filename}"
48
+ next if ignorable_key?(k)
49
+ expected = e[k]
50
+ actual = exif[k]
51
+ if actual.is_a?(String)
52
+ expected.downcase!
53
+ actual.downcase!
54
+ end
55
+ assert_equal expected, actual, "Key '#{k}' was incorrect for #{filename}"
46
56
  end
47
57
  end
48
58
 
59
+ TRANSLATED_KEY = /.*\-ml-\w\w-\w\w$/
60
+
61
+ def ignorable_key? key
62
+ @ignorable_keys ||= begin
63
+ ignorable = [:file_modify_date, :directory, :source_file, :exif_tool_version]
64
+ ignorable += [:modify_date, :create_date, :date_time_original] if Exiftoolr.exiftool_version <= 8.2
65
+ ignorable
66
+ end.include?(key) || key.to_s =~ TRANSLATED_KEY
67
+ end
68
+
49
69
  def test_multi_matches
50
70
  filenames = Dir["**/*.jpg"].to_a
51
71
  e = Exiftoolr.new(filenames)
@@ -55,7 +75,7 @@ class TestExiftoolr < Test::Unit::TestCase
55
75
  def test_error_filtering
56
76
  filenames = Dir["**/*.*"].to_a
57
77
  e = Exiftoolr.new(filenames)
58
- expected_files = Dir["**/*.jpg"].to_a.collect{|f|File.expand_path(f)}.sort
78
+ expected_files = Dir["**/*.jpg"].to_a.collect { |f| File.expand_path(f) }.sort
59
79
  assert_equal expected_files, e.files_with_results.sort
60
80
  filenames.each { |f| validate_result(e.result_for(f), f) }
61
81
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: exiftoolr
3
3
  version: !ruby/object:Gem::Version
4
- hash: 19
4
+ hash: 17
5
5
  prerelease:
6
6
  segments:
7
7
  - 0
8
8
  - 0
9
- - 6
10
- version: 0.0.6
9
+ - 7
10
+ version: 0.0.7
11
11
  platform: ruby
12
12
  authors:
13
13
  - Matthew McEachen
@@ -15,10 +15,11 @@ autorequire:
15
15
  bindir: bin
16
16
  cert_chain: []
17
17
 
18
- date: 2012-02-26 00:00:00 Z
18
+ date: 2012-03-28 00:00:00 -07:00
19
+ default_executable:
19
20
  dependencies:
20
21
  - !ruby/object:Gem::Dependency
21
- prerelease: false
22
+ type: :runtime
22
23
  requirement: &id001 !ruby/object:Gem::Requirement
23
24
  none: false
24
25
  requirements:
@@ -28,11 +29,11 @@ dependencies:
28
29
  segments:
29
30
  - 0
30
31
  version: "0"
31
- type: :runtime
32
+ prerelease: false
32
33
  name: json
33
34
  version_requirements: *id001
34
35
  - !ruby/object:Gem::Dependency
35
- prerelease: false
36
+ type: :development
36
37
  requirement: &id002 !ruby/object:Gem::Requirement
37
38
  none: false
38
39
  requirements:
@@ -42,23 +43,9 @@ dependencies:
42
43
  segments:
43
44
  - 0
44
45
  version: "0"
45
- type: :development
46
+ prerelease: false
46
47
  name: rake
47
48
  version_requirements: *id002
48
- - !ruby/object:Gem::Dependency
49
- prerelease: false
50
- requirement: &id003 !ruby/object:Gem::Requirement
51
- none: false
52
- requirements:
53
- - - ">="
54
- - !ruby/object:Gem::Version
55
- hash: 3
56
- segments:
57
- - 0
58
- version: "0"
59
- type: :development
60
- name: rspec
61
- version_requirements: *id003
62
49
  description: Multiget ExifTool wrapper for ruby
63
50
  email:
64
51
  - matthew-github@mceachen.org
@@ -70,6 +57,7 @@ extra_rdoc_files: []
70
57
 
71
58
  files:
72
59
  - .gitignore
60
+ - .travis.yml
73
61
  - Gemfile
74
62
  - README.md
75
63
  - Rakefile
@@ -86,6 +74,7 @@ files:
86
74
  - test/iPhone 4S.jpg
87
75
  - test/iPhone 4S.jpg.yaml
88
76
  - test/test_exiftoolr.rb
77
+ has_rdoc: true
89
78
  homepage: https://github.com/mceachen/exiftoolr
90
79
  licenses: []
91
80
 
@@ -115,7 +104,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
115
104
  requirements:
116
105
  - ExifTool (see http://www.sno.phy.queensu.ca/~phil/exiftool/)
117
106
  rubyforge_project: exiftoolr
118
- rubygems_version: 1.8.17
107
+ rubygems_version: 1.6.2
119
108
  signing_key:
120
109
  specification_version: 3
121
110
  summary: Multiget ExifTool wrapper for ruby