exiftoolr 0.0.6 → 0.0.7
Sign up to get free protection for your applications and to get access to all the features.
- data/.travis.yml +9 -0
- data/README.md +11 -3
- data/exiftoolr.gemspec +0 -1
- data/lib/exiftoolr.rb +22 -14
- data/lib/exiftoolr/result.rb +21 -12
- data/lib/exiftoolr/version.rb +1 -1
- data/test/Canon 20D.jpg.yaml +1 -3
- data/test/faces.jpg.yaml +3 -29
- data/test/test_exiftoolr.rb +23 -3
- metadata +12 -23
data/.travis.yml
ADDED
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.
|
75
|
+
### 0.0.7
|
74
76
|
|
75
|
-
Added
|
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)
|
data/exiftoolr.gemspec
CHANGED
data/lib/exiftoolr.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
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
|
data/lib/exiftoolr/result.rb
CHANGED
@@ -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 =
|
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,
|
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
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
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
|
data/lib/exiftoolr/version.rb
CHANGED
data/test/Canon 20D.jpg.yaml
CHANGED
@@ -166,9 +166,7 @@
|
|
166
166
|
:aperture: 9.0
|
167
167
|
:easy_mode: Manual
|
168
168
|
:nd_filter: n/a
|
169
|
-
:exposure_level_increments:
|
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
|
data/test/faces.jpg.yaml
CHANGED
@@ -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:
|
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:
|
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
|
-
:
|
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
|
data/test/test_exiftoolr.rb
CHANGED
@@ -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
|
45
|
-
|
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:
|
4
|
+
hash: 17
|
5
5
|
prerelease:
|
6
6
|
segments:
|
7
7
|
- 0
|
8
8
|
- 0
|
9
|
-
-
|
10
|
-
version: 0.0.
|
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-
|
18
|
+
date: 2012-03-28 00:00:00 -07:00
|
19
|
+
default_executable:
|
19
20
|
dependencies:
|
20
21
|
- !ruby/object:Gem::Dependency
|
21
|
-
|
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
|
-
|
32
|
+
prerelease: false
|
32
33
|
name: json
|
33
34
|
version_requirements: *id001
|
34
35
|
- !ruby/object:Gem::Dependency
|
35
|
-
|
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
|
-
|
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.
|
107
|
+
rubygems_version: 1.6.2
|
119
108
|
signing_key:
|
120
109
|
specification_version: 3
|
121
110
|
summary: Multiget ExifTool wrapper for ruby
|