exifr 0.9.6 → 0.10
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/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
|