exifr 0.9.6 → 0.10
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +3 -0
- data/README +16 -4
- data/Rakefile +3 -1
- data/bin/exifr +25 -10
- data/lib/exifr.rb +2 -3
- data/lib/jpeg.rb +12 -13
- data/lib/tiff.rb +440 -0
- data/tests/data/nikon_d1x.tif +0 -0
- data/tests/data/plain.tif +0 -0
- data/tests/test_helper.rb +8 -1
- data/tests/test_jpeg.rb +18 -8
- data/tests/test_tiff.rb +109 -0
- metadata +6 -5
- data/lib/exif.rb +0 -255
- data/lib/tiff_header.rb +0 -83
- data/tests/test_exif.rb +0 -61
data/CHANGELOG
CHANGED
data/README
CHANGED
@@ -1,12 +1,24 @@
|
|
1
1
|
= EXIF Reader
|
2
|
-
EXIF Reader is a module to read
|
2
|
+
EXIF Reader is a module to read metadata from JPEG and TIFF images.
|
3
3
|
|
4
4
|
== Examples
|
5
|
-
EXIFR::JPEG.new('
|
6
|
-
EXIFR::JPEG.new('
|
5
|
+
EXIFR::JPEG.new('IMG_6841.JPG').width # => 2272
|
6
|
+
EXIFR::JPEG.new('IMG_6841.JPG').height # => 1704
|
7
|
+
EXIFR::JPEG.new('IMG_6841.JPG').exif? # => true
|
8
|
+
EXIFR::JPEG.new('IMG_6841.JPG').model # => "Canon PowerShot G3"
|
9
|
+
EXIFR::JPEG.new('IMG_6841.JPG').date_time # => Fri Feb 09 16:48:54 +0100 2007
|
10
|
+
EXIFR::JPEG.new('IMG_6841.JPG').exposure_time.to_s # => "1/15"
|
11
|
+
EXIFR::JPEG.new('IMG_6841.JPG').f_number.to_f # => 2.0
|
12
|
+
|
13
|
+
EXIFR::TIFF.new('DSC_0218.TIF').width # => 3008
|
14
|
+
EXIFR::TIFF.new('DSC_0218.TIF')[1].width # => 160
|
15
|
+
EXIFR::TIFF.new('DSC_0218.TIF').model # => "NIKON D1X"
|
16
|
+
EXIFR::TIFF.new('DSC_0218.TIF').date_time # => Tue May 23 19:15:32 +0200 2006
|
17
|
+
EXIFR::TIFF.new('DSC_0218.TIF').exposure_time.to_s # => "1/100"
|
18
|
+
EXIFR::TIFF.new('DSC_0218.TIF').f_number.to_f # => 5.0
|
7
19
|
|
8
20
|
== Author
|
9
21
|
R.W. van 't Veer
|
10
22
|
|
11
23
|
== Copyright
|
12
|
-
Copyright (c) 2006 R.W. van 't Veer
|
24
|
+
Copyright (c) 2006, 2007 - R.W. van 't Veer
|
data/Rakefile
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# Copyright (c) 2006, 2007 - R.W. van 't Veer
|
2
|
+
|
1
3
|
require 'rake/rdoctask'
|
2
4
|
require 'rake/gempackagetask'
|
3
5
|
require 'rake/testtask'
|
@@ -6,7 +8,7 @@ task :default => :test
|
|
6
8
|
|
7
9
|
spec = Gem::Specification.new do |s|
|
8
10
|
s.name = 'exifr'
|
9
|
-
s.version = '0.
|
11
|
+
s.version = '0.10'
|
10
12
|
s.author = "R.W. van 't Veer"
|
11
13
|
s.email = 'remco@remvee.net'
|
12
14
|
s.homepage = 'http://exifr.rubyforge.org/'
|
data/bin/exifr
CHANGED
@@ -1,22 +1,36 @@
|
|
1
1
|
require 'exifr'
|
2
2
|
include EXIFR
|
3
3
|
|
4
|
-
def
|
4
|
+
def pp_jpeg(fname)
|
5
5
|
jpeg = JPEG.new(fname)
|
6
6
|
ks = %w(width height comment bits)
|
7
|
-
ks += jpeg.exif.keys.map{|a|a.to_s}.sort{|a,b|a<=>b} if jpeg.exif?
|
7
|
+
ks += jpeg.exif.to_hash.keys.map{|a|a.to_s}.sort{|a,b|a<=>b} if jpeg.exif?
|
8
8
|
|
9
9
|
l = []
|
10
10
|
ks[0..3].each do |k|
|
11
11
|
v = jpeg.send(k)
|
12
|
-
l << [k, v] if v
|
12
|
+
l << [k, v.inspect] if v
|
13
13
|
end
|
14
14
|
ks[4..-1].each do |k|
|
15
|
-
v = jpeg.exif[k.to_sym]
|
16
|
-
l << [k, v] if v
|
15
|
+
v = jpeg.exif.to_hash[k.to_sym]
|
16
|
+
l << [k, v.inspect] if v
|
17
17
|
end
|
18
|
-
l
|
19
|
-
|
18
|
+
pp(fname, l)
|
19
|
+
end
|
20
|
+
|
21
|
+
def pp_tiff(fname)
|
22
|
+
tiff = TIFF.new(fname)
|
23
|
+
tiff.each_with_index do |img,index|
|
24
|
+
l = []
|
25
|
+
l << ['width', img.width] << ['height', img.height]
|
26
|
+
img.to_hash.keys.map{|a|a.to_s}.sort{|a,b|a<=>b}.each do |key|
|
27
|
+
l << [key, img.to_hash[key.to_sym].inspect]
|
28
|
+
end
|
29
|
+
pp(tiff.size == 1 ? fname : "#{fname}[#{index}]", l)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def pp(fname, l)
|
20
34
|
puts "#{fname}:"
|
21
35
|
f = " %#{l.sort{|a,b|a[0].size <=> b[0].size}.last[0].size}s = %s\n"
|
22
36
|
l.each{|k,v|puts f % [k, [v].flatten.map{|t|t.to_s}.join(', ')]}
|
@@ -24,11 +38,12 @@ end
|
|
24
38
|
|
25
39
|
if ARGV.size == 0
|
26
40
|
STDERR.puts "Usage: #{$0} FILE .."
|
27
|
-
elsif ARGV.size == 1
|
28
|
-
pp ARGV[0]
|
29
41
|
else
|
30
42
|
ARGV.each do |fname|
|
31
|
-
|
43
|
+
case fname
|
44
|
+
when /\.(jpg|jpeg)$/i: pp_jpeg fname
|
45
|
+
when /\.(tif|tiff)$/i: pp_tiff fname
|
46
|
+
end
|
32
47
|
puts
|
33
48
|
end
|
34
49
|
end
|
data/lib/exifr.rb
CHANGED
data/lib/jpeg.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
|
-
# Copyright (c) 2006 - R.W. van 't Veer
|
1
|
+
# Copyright (c) 2006, 2007 - R.W. van 't Veer
|
2
2
|
|
3
|
-
require '
|
3
|
+
require 'stringio'
|
4
4
|
|
5
5
|
module EXIFR
|
6
6
|
# = JPEG decoder
|
@@ -13,15 +13,15 @@ module EXIFR
|
|
13
13
|
attr_reader :height
|
14
14
|
# image width
|
15
15
|
attr_reader :width
|
16
|
-
# number of bits per
|
17
|
-
attr_reader :bits
|
16
|
+
# number of bits per ??
|
17
|
+
attr_reader :bits # :nodoc:
|
18
18
|
# comment; a string if one comment found, an array if more,
|
19
19
|
# otherwise <tt>nil</tt>
|
20
20
|
attr_reader :comment
|
21
21
|
# EXIF data if available
|
22
22
|
attr_reader :exif
|
23
23
|
|
24
|
-
# +file+ is a filename or an IO object
|
24
|
+
# +file+ is a filename or an IO object.
|
25
25
|
def initialize(file)
|
26
26
|
if file.kind_of? String
|
27
27
|
File.open(file, 'rb') { |io| examine(io) }
|
@@ -30,18 +30,17 @@ module EXIFR
|
|
30
30
|
end
|
31
31
|
end
|
32
32
|
|
33
|
-
#
|
33
|
+
# Returns +true+ when EXIF data is available.
|
34
34
|
def exif?
|
35
35
|
!exif.nil?
|
36
36
|
end
|
37
37
|
|
38
|
-
#
|
38
|
+
# Dispath to EXIF. When no EXIF data is available but the +method+ does exist
|
39
|
+
# for EXIF data +nil+ will be returned.
|
39
40
|
def method_missing(method, *args)
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
super
|
44
|
-
end
|
41
|
+
super unless args.empty?
|
42
|
+
super unless TIFF::TAGS.include?(method)
|
43
|
+
@exif.send method if @exif
|
45
44
|
end
|
46
45
|
|
47
46
|
private
|
@@ -75,7 +74,7 @@ module EXIFR
|
|
75
74
|
@comment = @comment.first if @comment && @comment.size == 1
|
76
75
|
|
77
76
|
if app1 = app1s.find { |d| d[0..5] == "Exif\0\0" }
|
78
|
-
@exif =
|
77
|
+
@exif = TIFF.new(StringIO.new(app1[6..-1]))
|
79
78
|
end
|
80
79
|
end
|
81
80
|
end
|
data/lib/tiff.rb
ADDED
@@ -0,0 +1,440 @@
|
|
1
|
+
# Copyright (c) 2007 - R.W. van 't Veer
|
2
|
+
|
3
|
+
require 'rational'
|
4
|
+
|
5
|
+
module EXIFR
|
6
|
+
# = TIFF decoder
|
7
|
+
#
|
8
|
+
# == Date properties
|
9
|
+
# The properties <tt>:date_time</tt>, <tt>:date_time_original</tt>,
|
10
|
+
# <tt>:date_time_digitized</tt> coerced into Time objects.
|
11
|
+
#
|
12
|
+
# == Orientation
|
13
|
+
# The property <tt>:orientation</tt> describes the subject rotated and/or
|
14
|
+
# mirrored in relation to the camera. It is translated to one of the following
|
15
|
+
# modules:
|
16
|
+
# * TopLeftOrientation
|
17
|
+
# * TopRightOrientation
|
18
|
+
# * BottomRightOrientation
|
19
|
+
# * BottomLeftOrientation
|
20
|
+
# * LeftTopOrientation
|
21
|
+
# * RightTopOrientation
|
22
|
+
# * RightBottomOrientation
|
23
|
+
# * LeftBottomOrientation
|
24
|
+
#
|
25
|
+
# These modules have two methods:
|
26
|
+
# * <tt>to_i</tt>; return the original integer
|
27
|
+
# * <tt>transform_rmagick(image)</tt>; transforms the given RMagick::Image
|
28
|
+
# to a viewable version
|
29
|
+
#
|
30
|
+
# == Examples
|
31
|
+
# EXIFR::TIFF.new('DSC_0218.TIF').width # => 3008
|
32
|
+
# EXIFR::TIFF.new('DSC_0218.TIF')[1].width # => 160
|
33
|
+
# EXIFR::TIFF.new('DSC_0218.TIF').model # => "NIKON D1X"
|
34
|
+
# EXIFR::TIFF.new('DSC_0218.TIF').date_time # => Tue May 23 19:15:32 +0200 2006
|
35
|
+
# EXIFR::TIFF.new('DSC_0218.TIF').exposure_time # => Rational(1, 100)
|
36
|
+
# EXIFR::TIFF.new('DSC_0218.TIF').orientation # => EXIFR::TIFF::TopLeftOrientation
|
37
|
+
class TIFF
|
38
|
+
include Enumerable
|
39
|
+
|
40
|
+
TAG_MAPPING = {} # :nodoc:
|
41
|
+
TAG_MAPPING.merge!({
|
42
|
+
:image => {
|
43
|
+
0x00FE => :new_subfile_type,
|
44
|
+
0x00FF => :subfile_type,
|
45
|
+
0x0100 => :image_width,
|
46
|
+
0x0101 => :image_length,
|
47
|
+
0x0102 => :bits_per_sample,
|
48
|
+
0x0103 => :compression,
|
49
|
+
0x0106 => :photometric_interpretation,
|
50
|
+
0x0107 => :threshholding,
|
51
|
+
0x0108 => :cell_width,
|
52
|
+
0x0109 => :cell_length,
|
53
|
+
0x010a => :fill_order,
|
54
|
+
0x010d => :document_name,
|
55
|
+
0x010e => :image_description,
|
56
|
+
0x010f => :make,
|
57
|
+
0x0110 => :model,
|
58
|
+
0x0111 => :strip_offsets,
|
59
|
+
0x0112 => :orientation,
|
60
|
+
0x0115 => :samples_per_pixel,
|
61
|
+
0x0116 => :rows_per_strip,
|
62
|
+
0x0117 => :strip_byte_counts,
|
63
|
+
0x0118 => :min_sample_value,
|
64
|
+
0x0119 => :max_sample_value,
|
65
|
+
0x011a => :x_resolution,
|
66
|
+
0x011b => :y_resolution,
|
67
|
+
0x011c => :planar_configuration,
|
68
|
+
0x011d => :page_name,
|
69
|
+
0x011e => :x_position,
|
70
|
+
0x011f => :y_position,
|
71
|
+
0x0120 => :free_offsets,
|
72
|
+
0x0121 => :free_byte_counts,
|
73
|
+
0x0122 => :gray_response_unit,
|
74
|
+
0x0123 => :gray_response_curve,
|
75
|
+
0x0124 => :t4_options,
|
76
|
+
0x0125 => :t6_options,
|
77
|
+
0x0128 => :resolution_unit,
|
78
|
+
0x012d => :transfer_function,
|
79
|
+
0x0131 => :software,
|
80
|
+
0x0132 => :date_time,
|
81
|
+
0x013b => :artist,
|
82
|
+
0x013c => :host_computer,
|
83
|
+
0x013a => :predictor,
|
84
|
+
0x013e => :white_point,
|
85
|
+
0x013f => :primary_chromaticities,
|
86
|
+
0x0140 => :color_map,
|
87
|
+
0x0141 => :halftone_hints,
|
88
|
+
0x0142 => :tile_width,
|
89
|
+
0x0143 => :tile_length,
|
90
|
+
0x0144 => :tile_offsets,
|
91
|
+
0x0145 => :tile_byte_counts,
|
92
|
+
0x0146 => :bad_fax_lines,
|
93
|
+
0x0147 => :clean_fax_data,
|
94
|
+
0x0148 => :consecutive_bad_fax_lines,
|
95
|
+
0x014a => :sub_ifds,
|
96
|
+
0x014c => :ink_set,
|
97
|
+
0x014d => :ink_names,
|
98
|
+
0x014e => :number_of_inks,
|
99
|
+
0x0150 => :dot_range,
|
100
|
+
0x0151 => :target_printer,
|
101
|
+
0x0152 => :extra_samples,
|
102
|
+
0x0156 => :transfer_range,
|
103
|
+
0x0157 => :clip_path,
|
104
|
+
0x0158 => :x_clip_path_units,
|
105
|
+
0x0159 => :y_clip_path_units,
|
106
|
+
0x015a => :indexed,
|
107
|
+
0x015b => :jpeg_tables,
|
108
|
+
0x015f => :opi_proxy,
|
109
|
+
0x0190 => :global_parameters_ifd,
|
110
|
+
0x0191 => :profile_type,
|
111
|
+
0x0192 => :fax_profile,
|
112
|
+
0x0193 => :coding_methods,
|
113
|
+
0x0194 => :version_year,
|
114
|
+
0x0195 => :mode_number,
|
115
|
+
0x01B1 => :decode,
|
116
|
+
0x01B2 => :default_image_color,
|
117
|
+
0x0200 => :jpegproc,
|
118
|
+
0x0201 => :jpeg_interchange_format,
|
119
|
+
0x0202 => :jpeg_interchange_format_length,
|
120
|
+
0x0203 => :jpeg_restart_interval,
|
121
|
+
0x0205 => :jpeg_lossless_predictors,
|
122
|
+
0x0206 => :jpeg_point_transforms,
|
123
|
+
0x0207 => :jpeg_q_tables,
|
124
|
+
0x0208 => :jpeg_dc_tables,
|
125
|
+
0x0209 => :jpeg_ac_tables,
|
126
|
+
0x0211 => :ycb_cr_coefficients,
|
127
|
+
0x0212 => :ycb_cr_sub_sampling,
|
128
|
+
0x0213 => :ycb_cr_positioning,
|
129
|
+
0x0214 => :reference_black_white,
|
130
|
+
0x022F => :strip_row_counts,
|
131
|
+
0x02BC => :xmp,
|
132
|
+
0x800D => :image_id,
|
133
|
+
0x87AC => :image_layer,
|
134
|
+
0x8298 => :copyright,
|
135
|
+
0x83bb => :iptc,
|
136
|
+
|
137
|
+
0x8769 => :exif,
|
138
|
+
0x8825 => :gps,
|
139
|
+
},
|
140
|
+
|
141
|
+
:exif => {
|
142
|
+
0x829a => :exposure_time,
|
143
|
+
0x829d => :f_number,
|
144
|
+
0x8822 => :exposure_program,
|
145
|
+
0x8824 => :spectral_sensitivity,
|
146
|
+
0x8827 => :iso_speed_ratings,
|
147
|
+
0x8828 => :oecf,
|
148
|
+
0x9000 => :exif_version,
|
149
|
+
0x9003 => :date_time_original,
|
150
|
+
0x9004 => :date_time_digitized,
|
151
|
+
0x9101 => :components_configuration,
|
152
|
+
0x9102 => :compressed_bits_per_pixel,
|
153
|
+
0x9201 => :shutter_speed_value,
|
154
|
+
0x9202 => :aperture_value,
|
155
|
+
0x9203 => :brightness_value,
|
156
|
+
0x9204 => :exposure_bias_value,
|
157
|
+
0x9205 => :max_aperture_value,
|
158
|
+
0x9206 => :subject_distance,
|
159
|
+
0x9207 => :metering_mode,
|
160
|
+
0x9208 => :light_source,
|
161
|
+
0x9209 => :flash,
|
162
|
+
0x920a => :focal_length,
|
163
|
+
0x9214 => :subject_area,
|
164
|
+
0x927c => :maker_note,
|
165
|
+
0x9286 => :user_comment,
|
166
|
+
0x9290 => :subsec_time,
|
167
|
+
0x9291 => :subsec_time_orginal,
|
168
|
+
0x9292 => :subsec_time_digitized,
|
169
|
+
0xa000 => :flashpix_version,
|
170
|
+
0xa001 => :color_space,
|
171
|
+
0xa002 => :pixel_x_dimension,
|
172
|
+
0xa003 => :pixel_y_dimension,
|
173
|
+
0xa004 => :related_sound_file,
|
174
|
+
0xa20b => :flash_energy,
|
175
|
+
0xa20c => :spatial_frequency_response,
|
176
|
+
0xa20e => :focal_plane_x_resolution,
|
177
|
+
0xa20f => :focal_plane_y_resolution,
|
178
|
+
0xa210 => :focal_plane_resolution_unit,
|
179
|
+
0xa214 => :subject_location,
|
180
|
+
0xa215 => :exposure_index,
|
181
|
+
0xa217 => :sensing_method,
|
182
|
+
0xa300 => :file_source,
|
183
|
+
0xa301 => :scene_type,
|
184
|
+
0xa302 => :cfa_pattern,
|
185
|
+
0xa401 => :custom_rendered,
|
186
|
+
0xa402 => :exposure_mode,
|
187
|
+
0xa403 => :white_balance,
|
188
|
+
0xa404 => :digital_zoom_ratio,
|
189
|
+
0xa405 => :focal_length_in_35mm_film,
|
190
|
+
0xa406 => :scene_capture_type,
|
191
|
+
0xa407 => :gain_control,
|
192
|
+
0xa408 => :contrast,
|
193
|
+
0xa409 => :saturation,
|
194
|
+
0xa40a => :sharpness,
|
195
|
+
0xa40b => :device_setting_description,
|
196
|
+
0xa40c => :subject_distance_range,
|
197
|
+
0xa420 => :image_unique_id
|
198
|
+
},
|
199
|
+
|
200
|
+
:gps => {
|
201
|
+
0x0000 => :gps_version_id,
|
202
|
+
0x0001 => :gps_latitude_ref,
|
203
|
+
0x0002 => :gps_latitude,
|
204
|
+
0x0003 => :gps_longitude_ref,
|
205
|
+
0x0004 => :gps_longitude,
|
206
|
+
0x0005 => :gps_altitude_ref,
|
207
|
+
0x0006 => :gps_altitude ,
|
208
|
+
0x0007 => :gps_time_stamp,
|
209
|
+
0x0008 => :gps_satellites,
|
210
|
+
0x0009 => :gps_status,
|
211
|
+
0x000a => :gps_measure_mode,
|
212
|
+
0x000b => :gps_dop,
|
213
|
+
0x000c => :gps_speed_ref,
|
214
|
+
0x000d => :gps_speed,
|
215
|
+
0x000e => :gps_track_ref,
|
216
|
+
0x000f => :gps_track,
|
217
|
+
0x0010 => :gps_img_direction_ref,
|
218
|
+
0x0011 => :gps_img_direction,
|
219
|
+
0x0012 => :gps_map_datum,
|
220
|
+
0x0013 => :gps_dest_latitude_ref,
|
221
|
+
0x0014 => :gps_dest_latitude,
|
222
|
+
0x0015 => :gps_dest_longitude_ref,
|
223
|
+
0x0016 => :gps_dest_longitude,
|
224
|
+
0x0017 => :gps_dest_bearing_ref,
|
225
|
+
0x0018 => :gps_dest_bearing,
|
226
|
+
0x0019 => :gps_dest_distance_ref,
|
227
|
+
0x001a => :gps_dest_distance,
|
228
|
+
0x001b => :gps_processing_method,
|
229
|
+
0x001c => :gps_area_information,
|
230
|
+
0x001d => :gps_date_stamp,
|
231
|
+
0x001e => :gps_differential,
|
232
|
+
},
|
233
|
+
})
|
234
|
+
IFD_TAGS = [:image, :exif, :gps] # :nodoc:
|
235
|
+
|
236
|
+
time_proc = proc do |value|
|
237
|
+
if value =~ /^(\d{4}):(\d\d):(\d\d) (\d\d):(\d\d):(\d\d)$/
|
238
|
+
Time.mktime($1, $2, $3, $4, $5, $6) rescue nil
|
239
|
+
else
|
240
|
+
value
|
241
|
+
end
|
242
|
+
end
|
243
|
+
|
244
|
+
ORIENTATIONS = [] # :nodoc:
|
245
|
+
[
|
246
|
+
nil,
|
247
|
+
[:TopLeft, 'img'],
|
248
|
+
[:TopRight, 'img.flop'],
|
249
|
+
[:BottomRight, 'img.rotate(180)'],
|
250
|
+
[:BottomLeft, 'img.flip'],
|
251
|
+
[:LeftTop, 'img.rotate(90).flop'],
|
252
|
+
[:RightTop, 'img.rotate(90)'],
|
253
|
+
[:RightBottom, 'img.rotate(270).flop'],
|
254
|
+
[:LeftBottom, 'img.rotate(270)'],
|
255
|
+
].each_with_index do |tuple,index|
|
256
|
+
next unless tuple
|
257
|
+
name, rmagic_code = *tuple
|
258
|
+
|
259
|
+
eval <<-EOS
|
260
|
+
module #{name}Orientation
|
261
|
+
def self.to_i; #{index}; end
|
262
|
+
def self.transform_rmagick(img); #{rmagic_code}; end
|
263
|
+
end
|
264
|
+
ORIENTATIONS[#{index}] = #{name}Orientation
|
265
|
+
EOS
|
266
|
+
end
|
267
|
+
|
268
|
+
ADAPTERS = Hash.new { proc { |v| v } } # :nodoc:
|
269
|
+
ADAPTERS.merge!({
|
270
|
+
:date_time_original => time_proc,
|
271
|
+
:date_time_digitized => time_proc,
|
272
|
+
:date_time => time_proc,
|
273
|
+
:orientation => proc { |v| ORIENTATIONS[v] }
|
274
|
+
})
|
275
|
+
|
276
|
+
# Names for all recognized TIFF fields.
|
277
|
+
TAGS = [TAG_MAPPING.keys, TAG_MAPPING.values.map{|a|a.values}].flatten.uniq - IFD_TAGS
|
278
|
+
|
279
|
+
# +file+ is a filename or an IO object.
|
280
|
+
def initialize(file)
|
281
|
+
@data = file.respond_to?(:read) ? file.read : File.open(file, 'rb') { |io| io.read }
|
282
|
+
|
283
|
+
class << @data
|
284
|
+
attr_accessor :short, :long
|
285
|
+
def readshort(pos); self[pos..(pos + 1)].unpack(@short)[0]; end
|
286
|
+
def readlong(pos); self[pos..(pos + 3)].unpack(@long)[0]; end
|
287
|
+
end
|
288
|
+
|
289
|
+
case @data[0..1]
|
290
|
+
when 'II'; @data.short, @data.long = 'v', 'V'
|
291
|
+
when 'MM'; @data.short, @data.long = 'n', 'N'
|
292
|
+
else; raise 'no II or MM marker found'
|
293
|
+
end
|
294
|
+
|
295
|
+
@ifds = [IFD.new(@data)]
|
296
|
+
while ifd = @ifds.last.next; @ifds << ifd; end
|
297
|
+
end
|
298
|
+
|
299
|
+
# Number of images.
|
300
|
+
def size
|
301
|
+
@ifds.size
|
302
|
+
end
|
303
|
+
|
304
|
+
# Yield for each image.
|
305
|
+
def each
|
306
|
+
@ifds.each { |ifd| yield ifd }
|
307
|
+
end
|
308
|
+
|
309
|
+
# Get +index+ image.
|
310
|
+
def [](index)
|
311
|
+
@ifds[index]
|
312
|
+
end
|
313
|
+
|
314
|
+
# Dispatch to first image.
|
315
|
+
def method_missing(method, *args)
|
316
|
+
super unless args.empty?
|
317
|
+
|
318
|
+
if @ifds.first.respond_to?(method)
|
319
|
+
@ifds.first.send(method)
|
320
|
+
elsif TAGS.include?(method)
|
321
|
+
@ifds.first.to_hash[method]
|
322
|
+
else
|
323
|
+
super
|
324
|
+
end
|
325
|
+
end
|
326
|
+
|
327
|
+
# Convenience method to access image width.
|
328
|
+
def width; @ifds.first.width; end
|
329
|
+
|
330
|
+
# Convenience method to access image height.
|
331
|
+
def height; @ifds.first.height; end
|
332
|
+
|
333
|
+
# Get a hash presentation of the (first) image.
|
334
|
+
def to_hash; @ifds.first.to_hash; end
|
335
|
+
|
336
|
+
def inspect # :nodoc:
|
337
|
+
@ifds.inspect
|
338
|
+
end
|
339
|
+
|
340
|
+
class IFD # :nodoc:
|
341
|
+
attr_reader :type, :fields
|
342
|
+
|
343
|
+
def initialize(data, offset = nil, type = :image)
|
344
|
+
@data, @type, @fields = data, type, {}
|
345
|
+
|
346
|
+
pos = offset || @data.readlong(4)
|
347
|
+
num = @data.readshort(pos)
|
348
|
+
pos += 2
|
349
|
+
|
350
|
+
num.times do
|
351
|
+
add_field(Field.new(@data, pos))
|
352
|
+
pos += 12
|
353
|
+
end
|
354
|
+
|
355
|
+
@offset_next = @data.readlong(pos)
|
356
|
+
end
|
357
|
+
|
358
|
+
def method_missing(method, *args)
|
359
|
+
super unless args.empty? && TAGS.include?(method)
|
360
|
+
to_hash[method]
|
361
|
+
end
|
362
|
+
|
363
|
+
def width; image_width; end
|
364
|
+
def height; image_length; end
|
365
|
+
|
366
|
+
def to_hash
|
367
|
+
@hash ||= begin
|
368
|
+
result = @fields.dup
|
369
|
+
result.delete_if { |key,value| value.nil? }
|
370
|
+
result.each do |key,value|
|
371
|
+
if IFD_TAGS.include? key
|
372
|
+
result.merge!(value.to_hash)
|
373
|
+
result.delete key
|
374
|
+
end
|
375
|
+
end
|
376
|
+
end
|
377
|
+
end
|
378
|
+
|
379
|
+
def inspect
|
380
|
+
to_hash.inspect
|
381
|
+
end
|
382
|
+
|
383
|
+
def next
|
384
|
+
IFD.new(@data, @offset_next) unless @offset_next == 0
|
385
|
+
end
|
386
|
+
|
387
|
+
private
|
388
|
+
def add_field(field)
|
389
|
+
return unless tag = TAG_MAPPING[@type][field.tag]
|
390
|
+
if IFD_TAGS.include? tag
|
391
|
+
@fields[tag] = IFD.new(@data, field.offset, tag)
|
392
|
+
else
|
393
|
+
value = field.value.map { |v| ADAPTERS[tag][v] } if field.value
|
394
|
+
@fields[tag] = value.kind_of?(Array) && value.size == 1 ? value.first : value
|
395
|
+
end
|
396
|
+
end
|
397
|
+
end
|
398
|
+
|
399
|
+
class Field # :nodoc:
|
400
|
+
attr_reader :tag, :offset, :value
|
401
|
+
|
402
|
+
def initialize(data, pos)
|
403
|
+
@tag, count, @offset = data.readshort(pos), data.readlong(pos + 4), data.readlong(pos + 8)
|
404
|
+
|
405
|
+
case data.readshort(pos + 2)
|
406
|
+
when 1, 6 # byte, signed byte
|
407
|
+
# TODO handle signed bytes
|
408
|
+
len, pack = count, proc { |d| d }
|
409
|
+
when 2 # ascii
|
410
|
+
len, pack = count, proc { |d| d.strip }
|
411
|
+
when 3, 8 # short, signed short
|
412
|
+
# TODO handle signed
|
413
|
+
len, pack = count * 2, proc { |d| d.unpack(data.short + '*') }
|
414
|
+
when 4, 9 # long, signed long
|
415
|
+
# TODO handle signed
|
416
|
+
len, pack = count * 4, proc { |d| d.unpack(data.long + '*') }
|
417
|
+
when 5, 10
|
418
|
+
len, pack = count * 8, proc do |d|
|
419
|
+
r = []
|
420
|
+
d.unpack(data.long + '*').each_with_index do |v,i|
|
421
|
+
i % 2 == 0 ? r << [v] : r.last << v
|
422
|
+
end
|
423
|
+
r.map do |f|
|
424
|
+
if f[1] == 0 # allow NaN and Infinity
|
425
|
+
f[0].to_f.quo(f[1])
|
426
|
+
else
|
427
|
+
Rational.reduce(*f)
|
428
|
+
end
|
429
|
+
end
|
430
|
+
end
|
431
|
+
end
|
432
|
+
|
433
|
+
if len && pack
|
434
|
+
start = len > 4 ? @offset : (pos + 8)
|
435
|
+
@value = pack[data[start..(start + len - 1)]]
|
436
|
+
end
|
437
|
+
end
|
438
|
+
end
|
439
|
+
end
|
440
|
+
end
|
Binary file
|
Binary file
|
data/tests/test_helper.rb
CHANGED
@@ -1,4 +1,6 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
|
+
#
|
3
|
+
# Copyright (c) 2006, 2007 - R.W. van 't Veer
|
2
4
|
|
3
5
|
require 'test/unit'
|
4
6
|
require 'stringio'
|
@@ -9,14 +11,19 @@ require 'exifr'
|
|
9
11
|
include EXIFR
|
10
12
|
|
11
13
|
|
12
|
-
def
|
14
|
+
def all_test_jpegs
|
13
15
|
Dir[f('*.jpg')]
|
14
16
|
end
|
15
17
|
|
18
|
+
|
16
19
|
def all_test_exifs
|
17
20
|
Dir[f('*.exif')]
|
18
21
|
end
|
19
22
|
|
23
|
+
def all_test_tiffs
|
24
|
+
Dir[f('*.tif')]
|
25
|
+
end
|
26
|
+
|
20
27
|
def f(fname)
|
21
28
|
"#{File.dirname(__FILE__)}/data/#{fname}"
|
22
29
|
end
|
data/tests/test_jpeg.rb
CHANGED
@@ -1,10 +1,12 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
|
+
#
|
3
|
+
# Copyright (c) 2006, 2007 - R.W. van 't Veer
|
2
4
|
|
3
5
|
require File.join(File.dirname(__FILE__), 'test_helper')
|
4
6
|
|
5
7
|
class TestJPEG < Test::Unit::TestCase
|
6
8
|
def test_initialize
|
7
|
-
|
9
|
+
all_test_jpegs.each do |fname|
|
8
10
|
assert_nothing_raised do
|
9
11
|
JPEG.new(fname)
|
10
12
|
end
|
@@ -38,16 +40,24 @@ class TestJPEG < Test::Unit::TestCase
|
|
38
40
|
def test_exif
|
39
41
|
assert ! JPEG.new(f('image.jpg')).exif?
|
40
42
|
assert JPEG.new(f('exif.jpg')).exif?
|
43
|
+
assert_not_nil JPEG.new(f('exif.jpg')).exif.date_time
|
44
|
+
assert_not_nil JPEG.new(f('exif.jpg')).exif.f_number
|
41
45
|
end
|
42
46
|
|
43
|
-
def
|
44
|
-
|
47
|
+
def test_exif_dispatch
|
48
|
+
j = JPEG.new(f('exif.jpg'))
|
49
|
+
assert_not_nil j.date_time
|
50
|
+
assert_kind_of Time, j.date_time
|
51
|
+
assert_not_nil j.f_number
|
52
|
+
assert_kind_of Rational, j.f_number
|
45
53
|
end
|
46
54
|
|
47
|
-
def
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
55
|
+
def test_no_method_error
|
56
|
+
assert_nothing_raised { JPEG.new(f('image.jpg')).f_number }
|
57
|
+
assert_raise(NoMethodError) { JPEG.new(f('image.jpg')).foo }
|
58
|
+
end
|
59
|
+
|
60
|
+
def test_multiple_app1
|
61
|
+
assert JPEG.new(f('multiple-app1.jpg')).exif?
|
52
62
|
end
|
53
63
|
end
|
data/tests/test_tiff.rb
ADDED
@@ -0,0 +1,109 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
#
|
3
|
+
# Copyright (c) 2006, 2007 - R.W. van 't Veer
|
4
|
+
|
5
|
+
require File.join(File.dirname(__FILE__), 'test_helper')
|
6
|
+
|
7
|
+
class TestTIFF < Test::Unit::TestCase
|
8
|
+
def setup
|
9
|
+
@t = TIFF.new(f('nikon_d1x.tif'))
|
10
|
+
end
|
11
|
+
|
12
|
+
def test_initialize
|
13
|
+
all_test_tiffs.each do |fname|
|
14
|
+
assert_nothing_raised do
|
15
|
+
TIFF.new(fname)
|
16
|
+
end
|
17
|
+
assert_nothing_raised do
|
18
|
+
open(fname) { |rd| TIFF.new(rd) }
|
19
|
+
end
|
20
|
+
assert_nothing_raised do
|
21
|
+
TIFF.new(StringIO.new(File.read(fname)))
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def test_multiple_images
|
27
|
+
assert_equal 2, @t.size
|
28
|
+
end
|
29
|
+
|
30
|
+
def test_size
|
31
|
+
assert_equal 269, @t.image_width
|
32
|
+
assert_equal 269, @t.image_length
|
33
|
+
assert_equal 269, @t.width
|
34
|
+
assert_equal 269, @t.height
|
35
|
+
assert_equal 120, @t[1].image_width
|
36
|
+
assert_equal 160, @t[1].image_length
|
37
|
+
assert_equal 120, @t[1].width
|
38
|
+
assert_equal 160, @t[1].height
|
39
|
+
|
40
|
+
@t = TIFF.new(f('plain.tif'))
|
41
|
+
assert_equal 23, @t.image_width
|
42
|
+
assert_equal 24, @t.image_length
|
43
|
+
assert_equal 23, @t.width
|
44
|
+
assert_equal 24, @t.height
|
45
|
+
end
|
46
|
+
|
47
|
+
def test_enumerable
|
48
|
+
assert_equal @t[1], @t.find { |i| i.f_number.nil? }
|
49
|
+
end
|
50
|
+
|
51
|
+
def test_misc_fields
|
52
|
+
assert_equal 'Canon PowerShot G3', TIFF.new(f('canon-g3.exif')).model
|
53
|
+
end
|
54
|
+
|
55
|
+
def test_dates
|
56
|
+
(all_test_tiffs - [f('weird_date.exif'), f('plain.tif')]).each do |fname|
|
57
|
+
assert_kind_of Time, TIFF.new(fname).date_time
|
58
|
+
end
|
59
|
+
assert_nil TIFF.new(f('weird_date.exif')).date_time
|
60
|
+
end
|
61
|
+
|
62
|
+
def test_orientation
|
63
|
+
all_test_exifs.each do |fname|
|
64
|
+
orientation = TIFF.new(fname).orientation
|
65
|
+
assert_kind_of Module, orientation
|
66
|
+
assert orientation.respond_to?(:to_i)
|
67
|
+
assert orientation.respond_to?(:transform_rmagick)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def test_gps
|
72
|
+
t = TIFF.new(f('gps.exif'))
|
73
|
+
assert_equal "\2\2\0\0", t.gps_version_id
|
74
|
+
assert_equal 'N', t.gps_latitude_ref
|
75
|
+
assert_equal 'W', t.gps_longitude_ref
|
76
|
+
assert_equal [5355537.quo(100000), 0.quo(1), 0.quo(1)], t.gps_latitude
|
77
|
+
assert_equal [678886.quo(100000), 0.quo(1), 0.quo(1)], t.gps_longitude
|
78
|
+
assert_equal 'WGS84', t.gps_map_datum
|
79
|
+
|
80
|
+
(all_test_exifs - [f('gps.exif')]).each do |fname|
|
81
|
+
assert_nil TIFF.new(fname).gps_version_id
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def test_ifd_dispatch
|
86
|
+
assert_not_nil @t.f_number
|
87
|
+
assert_kind_of Rational, @t.f_number
|
88
|
+
assert_not_nil @t[0].f_number
|
89
|
+
assert_kind_of Rational, @t[0].f_number
|
90
|
+
end
|
91
|
+
|
92
|
+
def test_avoid_dispatch_to_nonexistent_ifds
|
93
|
+
assert_nothing_raised do
|
94
|
+
all_test_tiffs.each do |fname|
|
95
|
+
t = TIFF.new(fname)
|
96
|
+
TIFF::TAGS.each { |tag| t.send(tag) }
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
def test_to_hash
|
102
|
+
all_test_tiffs.each do |fname|
|
103
|
+
t = TIFF.new(fname)
|
104
|
+
TIFF::TAGS.each do |key|
|
105
|
+
assert_equal t.send(key), t.to_hash[key]
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
metadata
CHANGED
@@ -3,8 +3,8 @@ rubygems_version: 0.9.2
|
|
3
3
|
specification_version: 1
|
4
4
|
name: exifr
|
5
5
|
version: !ruby/object:Gem::Version
|
6
|
-
version: 0.
|
7
|
-
date: 2007-02-
|
6
|
+
version: "0.10"
|
7
|
+
date: 2007-02-27 00:00:00 +01:00
|
8
8
|
summary: EXIF Reader is a module to read EXIF from JPEG images.
|
9
9
|
require_paths:
|
10
10
|
- lib
|
@@ -31,14 +31,13 @@ authors:
|
|
31
31
|
files:
|
32
32
|
- Rakefile
|
33
33
|
- bin/exifr
|
34
|
-
- lib/exif.rb
|
35
34
|
- lib/exifr.rb
|
36
35
|
- lib/jpeg.rb
|
37
|
-
- lib/
|
36
|
+
- lib/tiff.rb
|
38
37
|
- tests/data
|
39
|
-
- tests/test_exif.rb
|
40
38
|
- tests/test_helper.rb
|
41
39
|
- tests/test_jpeg.rb
|
40
|
+
- tests/test_tiff.rb
|
42
41
|
- tests/data/1x1.jpg
|
43
42
|
- tests/data/canon-g3.exif
|
44
43
|
- tests/data/Canon_PowerShot_A85.exif
|
@@ -48,7 +47,9 @@ files:
|
|
48
47
|
- tests/data/gps.exif
|
49
48
|
- tests/data/image.jpg
|
50
49
|
- tests/data/multiple-app1.jpg
|
50
|
+
- tests/data/nikon_d1x.tif
|
51
51
|
- tests/data/Panasonic-DMC-LC33.exif
|
52
|
+
- tests/data/plain.tif
|
52
53
|
- tests/data/Trust-DC3500_MINI.exif
|
53
54
|
- tests/data/weird_date.exif
|
54
55
|
- README
|
data/lib/exif.rb
DELETED
@@ -1,255 +0,0 @@
|
|
1
|
-
#!/usr/bin/env ruby
|
2
|
-
# Copyright (c) 2006 - R.W. van 't Veer
|
3
|
-
|
4
|
-
module EXIFR
|
5
|
-
# = EXIF decoder
|
6
|
-
#
|
7
|
-
# The EXIF class contains the EXIF properties.
|
8
|
-
#
|
9
|
-
#
|
10
|
-
# == Date properties
|
11
|
-
#
|
12
|
-
# The properties <tt>:date_time</tt>, <tt>:date_time_original</tt>,
|
13
|
-
# <tt>:date_time_digitized</tt> are stored in a EXIF tags as an ASCII
|
14
|
-
# string. This class stores them as Time objects.
|
15
|
-
#
|
16
|
-
#
|
17
|
-
# == Orientation
|
18
|
-
#
|
19
|
-
# The property <tt>:orientation</tt> describes the subject rotated and/or
|
20
|
-
# mirrored in relation to the camera. The value is stored in an EXIF tags
|
21
|
-
# as an integer. This class stores this value as a module;
|
22
|
-
#
|
23
|
-
# * TopLeftOrientation
|
24
|
-
# * TopRightOrientation
|
25
|
-
# * BottomRightOrientation
|
26
|
-
# * BottomLeftOrientation
|
27
|
-
# * LeftTopOrientation
|
28
|
-
# * RightTopOrientation
|
29
|
-
# * RightBottomOrientation
|
30
|
-
# * LeftBottomOrientation
|
31
|
-
#
|
32
|
-
# These modules have two methods:
|
33
|
-
# * <tt>to_i</tt>; return the original EXIF tag integer
|
34
|
-
# * <tt>transform_rmagick(image)</tt>; transforms the given RMagick::Image
|
35
|
-
# to a viewable version
|
36
|
-
#
|
37
|
-
class EXIF < Hash
|
38
|
-
TAGS = {} # :nodoc:
|
39
|
-
TAGS.merge!({
|
40
|
-
:exif => {
|
41
|
-
0x0100 => :image_width,
|
42
|
-
0x0101 => :image_length,
|
43
|
-
0x0102 => :bits_per_sample,
|
44
|
-
0x0103 => :compression,
|
45
|
-
0x0106 => :photometric_interpretation,
|
46
|
-
0x010a => :fill_order,
|
47
|
-
0x010d => :document_name,
|
48
|
-
0x010e => :image_description,
|
49
|
-
0x010f => :make,
|
50
|
-
0x0110 => :model,
|
51
|
-
0x0111 => :strip_offsets,
|
52
|
-
0x0112 => :orientation,
|
53
|
-
0x0115 => :samples_per_pixel,
|
54
|
-
0x0116 => :rows_per_strip,
|
55
|
-
0x0117 => :strip_byte_counts,
|
56
|
-
0x011a => :xresolution,
|
57
|
-
0x011b => :yresolution,
|
58
|
-
0x011c => :planar_configuration,
|
59
|
-
0x0128 => :resolution_unit,
|
60
|
-
0x012d => :transfer_function,
|
61
|
-
0x0131 => :software,
|
62
|
-
0x0132 => :date_time,
|
63
|
-
0x013b => :artist,
|
64
|
-
0x013e => :white_point,
|
65
|
-
0x013f => :primary_chromaticities,
|
66
|
-
0x0156 => :transfer_range,
|
67
|
-
0x0200 => :jpegproc,
|
68
|
-
0x0201 => :jpeg_interchange_format,
|
69
|
-
0x0202 => :jpeg_interchange_format_length,
|
70
|
-
0x0211 => :ycb_cr_coefficients,
|
71
|
-
0x0212 => :ycb_cr_sub_sampling,
|
72
|
-
0x0213 => :ycb_cr_positioning,
|
73
|
-
0x0214 => :reference_black_white,
|
74
|
-
0x828d => :cfarepeat_pattern_dim,
|
75
|
-
0x828e => :cfapattern,
|
76
|
-
0x828f => :battery_level,
|
77
|
-
0x8298 => :copyright,
|
78
|
-
0x829a => :exposure_time,
|
79
|
-
0x829d => :fnumber,
|
80
|
-
0x83bb => :iptc_naa,
|
81
|
-
0x8769 => :exif,
|
82
|
-
0x8773 => :inter_color_profile,
|
83
|
-
0x8822 => :exposure_program,
|
84
|
-
0x8824 => :spectral_sensitivity,
|
85
|
-
0x8825 => :gps,
|
86
|
-
0x8827 => :isospeed_ratings,
|
87
|
-
0x8828 => :oecf,
|
88
|
-
0x9000 => :exif_version,
|
89
|
-
0x9003 => :date_time_original,
|
90
|
-
0x9004 => :date_time_digitized,
|
91
|
-
0x9101 => :components_configuration,
|
92
|
-
0x9102 => :compressed_bits_per_pixel,
|
93
|
-
0x9201 => :shutter_speed_value,
|
94
|
-
0x9202 => :aperture_value,
|
95
|
-
0x9203 => :brightness_value,
|
96
|
-
0x9204 => :exposure_bias_value,
|
97
|
-
0x9205 => :max_aperture_value,
|
98
|
-
0x9206 => :subject_distance,
|
99
|
-
0x9207 => :metering_mode,
|
100
|
-
0x9208 => :light_source,
|
101
|
-
0x9209 => :flash,
|
102
|
-
0x920a => :focal_length,
|
103
|
-
0x9214 => :subject_area,
|
104
|
-
0x927c => :maker_note,
|
105
|
-
0x9286 => :user_comment,
|
106
|
-
0x9290 => :subsec_time,
|
107
|
-
0x9291 => :subsec_time_orginal,
|
108
|
-
0x9292 => :subsec_time_digitized,
|
109
|
-
0xa000 => :flash_pix_version,
|
110
|
-
0xa001 => :color_space,
|
111
|
-
0xa002 => :pixel_xdimension,
|
112
|
-
0xa003 => :pixel_ydimension,
|
113
|
-
0xa004 => :related_sound_file,
|
114
|
-
0xa005 => :interoperability,
|
115
|
-
0xa20b => :flash_energy,
|
116
|
-
0xa20c => :spatial_frequency_response,
|
117
|
-
0xa20e => :focal_plane_xresolution,
|
118
|
-
0xa20f => :focal_plane_yresolution,
|
119
|
-
0xa210 => :focal_plane_resolution_unit,
|
120
|
-
0xa214 => :subject_location,
|
121
|
-
0xa215 => :exposure_index,
|
122
|
-
0xa217 => :sensing_method,
|
123
|
-
0xa300 => :file_source,
|
124
|
-
0xa301 => :scene_type,
|
125
|
-
0xa302 => :cfapattern,
|
126
|
-
0xa401 => :custom_rendered,
|
127
|
-
0xa402 => :exposure_mode,
|
128
|
-
0xa403 => :white_balance,
|
129
|
-
0xa404 => :digital_zoom_ratio,
|
130
|
-
0xa405 => :focal_len_in_35mm_film,
|
131
|
-
0xa406 => :scene_capture_type,
|
132
|
-
0xa407 => :gain_control,
|
133
|
-
0xa408 => :contrast,
|
134
|
-
0xa409 => :saturation,
|
135
|
-
0xa40a => :sharpness,
|
136
|
-
0xa40b => :device_setting_descr,
|
137
|
-
0xa40c => :subject_dist_range,
|
138
|
-
0xa420 => :image_unique_id
|
139
|
-
},
|
140
|
-
|
141
|
-
:gps => {
|
142
|
-
0x0000 => :gps_version_id,
|
143
|
-
0x0001 => :gps_latitude_ref,
|
144
|
-
0x0002 => :gps_latitude,
|
145
|
-
0x0003 => :gps_longitude_ref,
|
146
|
-
0x0004 => :gps_longitude,
|
147
|
-
0x0005 => :gps_altitude_ref,
|
148
|
-
0x0006 => :gps_altitude ,
|
149
|
-
0x0007 => :gps_time_stamp,
|
150
|
-
0x0008 => :gps_satellites,
|
151
|
-
0x0009 => :gps_status,
|
152
|
-
0x000a => :gps_measure_mode,
|
153
|
-
0x000b => :gpsdop,
|
154
|
-
0x000c => :gps_speed_ref,
|
155
|
-
0x000d => :gps_speed ,
|
156
|
-
0x000e => :gps_track_ref,
|
157
|
-
0x000f => :gps_track,
|
158
|
-
0x0010 => :gps_img_direction_ref,
|
159
|
-
0x0011 => :gps_img_direction,
|
160
|
-
0x0012 => :gps_map_datum,
|
161
|
-
0x0013 => :gps_dest_latitude_ref,
|
162
|
-
0x0014 => :gps_dest_latitude,
|
163
|
-
0x0015 => :gps_dest_longitude_ref,
|
164
|
-
0x0016 => :gps_dest_longitude,
|
165
|
-
0x0017 => :gps_dest_bearing_ref,
|
166
|
-
0x0018 => :gps_dest_bearing,
|
167
|
-
0x0019 => :gps_dest_distance_ref,
|
168
|
-
0x001a => :gps_dest_distance,
|
169
|
-
0x001b => :gps_processing_method,
|
170
|
-
0x001c => :gps_area_information,
|
171
|
-
0x001d => :gps_date_stamp,
|
172
|
-
0x001e => :gps_differential,
|
173
|
-
},
|
174
|
-
|
175
|
-
:interoperability => {
|
176
|
-
0x0001 => :interoperability_index
|
177
|
-
}
|
178
|
-
})
|
179
|
-
EXIF_HEADERS = [:exif, :gps, :interoperability] # :nodoc:
|
180
|
-
|
181
|
-
time_proc = proc do |value|
|
182
|
-
if value =~ /^(\d{4}):(\d\d):(\d\d) (\d\d):(\d\d):(\d\d)$/
|
183
|
-
Time.mktime($1, $2, $3, $4, $5, $6) rescue nil
|
184
|
-
else
|
185
|
-
value
|
186
|
-
end
|
187
|
-
end
|
188
|
-
|
189
|
-
ORIENTATIONS = [] # :nodoc:
|
190
|
-
[
|
191
|
-
nil,
|
192
|
-
[:TopLeft, 'img'],
|
193
|
-
[:TopRight, 'img.flop'],
|
194
|
-
[:BottomRight, 'img.rotate(180)'],
|
195
|
-
[:BottomLeft, 'img.flip'],
|
196
|
-
[:LeftTop, 'img.rotate(90).flop'],
|
197
|
-
[:RightTop, 'img.rotate(90)'],
|
198
|
-
[:RightBottom, 'img.rotate(270).flop'],
|
199
|
-
[:LeftBottom, 'img.rotate(270)'],
|
200
|
-
].each_with_index do |tuple,index|
|
201
|
-
next unless tuple
|
202
|
-
name, rmagic_code = *tuple
|
203
|
-
|
204
|
-
eval <<-EOS
|
205
|
-
module #{name}Orientation
|
206
|
-
def self.to_i; #{index}; end
|
207
|
-
def self.transform_rmagick(img); #{rmagic_code}; end
|
208
|
-
end
|
209
|
-
ORIENTATIONS[#{index}] = #{name}Orientation
|
210
|
-
EOS
|
211
|
-
end
|
212
|
-
|
213
|
-
ADAPTERS = Hash.new { proc { |v| v } } # :nodoc:
|
214
|
-
ADAPTERS.merge!({
|
215
|
-
:date_time_original => time_proc,
|
216
|
-
:date_time_digitized => time_proc,
|
217
|
-
:date_time => time_proc,
|
218
|
-
:orientation => proc { |v| ORIENTATIONS[v] }
|
219
|
-
})
|
220
|
-
|
221
|
-
# +data+ the content of the JPEG APP1 frame without the EXIF marker
|
222
|
-
def initialize(data)
|
223
|
-
traverse(data)
|
224
|
-
pull_thumbnail(data)
|
225
|
-
freeze
|
226
|
-
end
|
227
|
-
|
228
|
-
# thumbnail, if included
|
229
|
-
attr_reader :thumbnail
|
230
|
-
|
231
|
-
# convience; <tt>self[method]</tt>
|
232
|
-
def method_missing(method, *args)
|
233
|
-
self[method]
|
234
|
-
end
|
235
|
-
|
236
|
-
private
|
237
|
-
def traverse(data, offset = nil, ifd = :exif)
|
238
|
-
TiffHeader.new(data, offset).fields.each do |f|
|
239
|
-
tag = TAGS[ifd][f.tag]
|
240
|
-
value = f.value.map { |v| ADAPTERS[tag][v] } if f.value
|
241
|
-
value = (value.kind_of?(Array) && value.size == 1) ? value.first : value
|
242
|
-
if EXIF_HEADERS.include?(tag)
|
243
|
-
traverse(data, f.offset, tag)
|
244
|
-
elsif tag
|
245
|
-
self[tag] = value
|
246
|
-
end
|
247
|
-
end
|
248
|
-
end
|
249
|
-
|
250
|
-
def pull_thumbnail(data)
|
251
|
-
start, length = self[:jpeg_interchange_format], self[:jpeg_interchange_format_length]
|
252
|
-
@thumbnail = data[start..(start + length)] if start && length
|
253
|
-
end
|
254
|
-
end
|
255
|
-
end
|
data/lib/tiff_header.rb
DELETED
@@ -1,83 +0,0 @@
|
|
1
|
-
# Copyright (c) 2006 - R.W. van 't Veer
|
2
|
-
|
3
|
-
module EXIFR
|
4
|
-
class TiffHeader # :nodoc:
|
5
|
-
attr_reader :data, :fields
|
6
|
-
|
7
|
-
def initialize(data, offset = nil)
|
8
|
-
@data = data
|
9
|
-
@fields = []
|
10
|
-
|
11
|
-
unless @data.respond_to? :readshort
|
12
|
-
class << @data
|
13
|
-
attr_accessor :short, :long
|
14
|
-
def readshort(pos); self[pos..(pos + 1)].unpack(@short)[0]; end
|
15
|
-
def readlong(pos); self[pos..(pos + 3)].unpack(@long)[0]; end
|
16
|
-
end
|
17
|
-
|
18
|
-
case @data[0..1]
|
19
|
-
when 'II'; @data.short, @data.long = 'v', 'V'
|
20
|
-
when 'MM'; @data.short, @data.long = 'n', 'N'
|
21
|
-
else; raise 'no II or MM marker found'
|
22
|
-
end
|
23
|
-
end
|
24
|
-
|
25
|
-
readIfds(offset || @data.readlong(4))
|
26
|
-
end
|
27
|
-
|
28
|
-
def readIfds(pos)
|
29
|
-
while pos != 0 do
|
30
|
-
num = @data.readshort(pos)
|
31
|
-
pos += 2
|
32
|
-
|
33
|
-
num.times do
|
34
|
-
fields << TiffField.new(@data, pos)
|
35
|
-
pos += 12
|
36
|
-
end
|
37
|
-
|
38
|
-
pos = @data.readlong(pos)
|
39
|
-
end
|
40
|
-
end
|
41
|
-
end
|
42
|
-
|
43
|
-
class TiffField # :nodoc:
|
44
|
-
attr_reader :tag, :offset, :value
|
45
|
-
|
46
|
-
def initialize(data, pos)
|
47
|
-
@tag, count, @offset = data.readshort(pos), data.readlong(pos + 4), data.readlong(pos + 8)
|
48
|
-
|
49
|
-
case data.readshort(pos + 2)
|
50
|
-
when 1, 6 # byte, signed byte
|
51
|
-
# TODO handle signed bytes
|
52
|
-
len, pack = count, proc { |d| d }
|
53
|
-
when 2 # ascii
|
54
|
-
len, pack = count, proc { |d| d.strip }
|
55
|
-
when 3, 8 # short, signed short
|
56
|
-
# TODO handle signed
|
57
|
-
len, pack = count * 2, proc { |d| d.unpack(data.short + '*') }
|
58
|
-
when 4, 9 # long, signed long
|
59
|
-
# TODO handle signed
|
60
|
-
len, pack = count * 4, proc { |d| d.unpack(data.long + '*') }
|
61
|
-
when 5, 10
|
62
|
-
len, pack = count * 8, proc do |d|
|
63
|
-
r = []
|
64
|
-
d.unpack(data.long + '*').each_with_index do |v,i|
|
65
|
-
i % 2 == 0 ? r << [v] : r.last << v
|
66
|
-
end
|
67
|
-
r.map do |f|
|
68
|
-
if f[1] == 0 # allow NaN and Infinity
|
69
|
-
f[0].to_f.quo(f[1])
|
70
|
-
else
|
71
|
-
Rational.reduce(*f)
|
72
|
-
end
|
73
|
-
end
|
74
|
-
end
|
75
|
-
end
|
76
|
-
|
77
|
-
if len && pack
|
78
|
-
start = len > 4 ? @offset : (pos + 8)
|
79
|
-
@value = pack[data[start..(start + len - 1)]]
|
80
|
-
end
|
81
|
-
end
|
82
|
-
end
|
83
|
-
end
|
data/tests/test_exif.rb
DELETED
@@ -1,61 +0,0 @@
|
|
1
|
-
#!/usr/bin/env ruby
|
2
|
-
|
3
|
-
require File.join(File.dirname(__FILE__), 'test_helper')
|
4
|
-
|
5
|
-
class TestEXIF < Test::Unit::TestCase
|
6
|
-
def test_initialize
|
7
|
-
[[f('canon-g3.exif'), 'Canon PowerShot G3']].each do |fname,model|
|
8
|
-
assert_equal EXIF.new(File.read(fname)).model, model
|
9
|
-
end
|
10
|
-
|
11
|
-
assert_raise RuntimeError, 'no II or MM marker found' do
|
12
|
-
EXIF.new('X' * 100)
|
13
|
-
end
|
14
|
-
end
|
15
|
-
|
16
|
-
def test_dates
|
17
|
-
(all_test_exifs - [f('weird_date.exif')]).each do |fname|
|
18
|
-
assert_kind_of Time, EXIF.new(File.read(fname)).date_time
|
19
|
-
end
|
20
|
-
assert_nil EXIF.new(File.read(f('weird_date.exif'))).date_time
|
21
|
-
end
|
22
|
-
|
23
|
-
def test_orientation
|
24
|
-
all_test_exifs.each do |fname|
|
25
|
-
orientation = EXIF.new(File.read(fname)).orientation
|
26
|
-
assert_kind_of Module, orientation
|
27
|
-
assert orientation.respond_to?(:to_i)
|
28
|
-
assert orientation.respond_to?(:transform_rmagick)
|
29
|
-
end
|
30
|
-
end
|
31
|
-
|
32
|
-
def test_thumbnail
|
33
|
-
assert_not_nil JPEG.new(f('exif.jpg')).exif.thumbnail
|
34
|
-
|
35
|
-
all_test_exifs.each do |fname|
|
36
|
-
thumbnail = EXIF.new(File.read(fname)).thumbnail
|
37
|
-
assert_nothing_raised do
|
38
|
-
JPEG.new(StringIO.new(thumbnail))
|
39
|
-
end
|
40
|
-
end
|
41
|
-
end
|
42
|
-
|
43
|
-
def test_exif_offset
|
44
|
-
assert JPEG.new(f('exif.jpg')).exif.include?(:exif_version)
|
45
|
-
end
|
46
|
-
|
47
|
-
def test_gps
|
48
|
-
exif = EXIF.new(File.read(f('gps.exif')))
|
49
|
-
assert exif.include?(:gps_version_id)
|
50
|
-
assert_equal "\2\2\0\0", exif.gps_version_id
|
51
|
-
assert_equal 'N', exif.gps_latitude_ref
|
52
|
-
assert_equal 'W', exif.gps_longitude_ref
|
53
|
-
assert_equal [5355537.quo(100000), 0.quo(1), 0.quo(1)], exif.gps_latitude
|
54
|
-
assert_equal [678886.quo(100000), 0.quo(1), 0.quo(1)], exif.gps_longitude
|
55
|
-
assert_equal 'WGS84', exif.gps_map_datum
|
56
|
-
|
57
|
-
(all_test_exifs - [f('gps.exif')]).each do |fname|
|
58
|
-
assert EXIF.new(File.read(fname)).keys.map{|k|k.to_s}.grep(/gps/).empty?
|
59
|
-
end
|
60
|
-
end
|
61
|
-
end
|