exiftoolr 0.0.6 → 0.0.7

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.
@@ -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