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.
- 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
|
+
[](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
|