remvee-exifr 0.10.6.1

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG ADDED
@@ -0,0 +1,63 @@
1
+ EXIF Reader 0.10.6.1
2
+ * moved to GitHub
3
+
4
+ EXIF Reader 0.10.6
5
+ * bug fix (thanks to Forian Munz for reporting it); endless loop when reading a malformed EXIF/TIFF
6
+
7
+ EXIF Reader 0.10.5
8
+ * bug fix; "[#15421] duplicate orientation field behavior", first field (of any type) is leading now
9
+ * Ruby 1.9 compatible
10
+
11
+ EXIF Reader 0.10.4
12
+ * Thumbnail extraction; [#15317] Please add thumbnail extraction
13
+
14
+ EXIF Reader 0.10.3
15
+ * YAML friendly; can now safely (de)serialize
16
+
17
+ EXIF Reader 0.10.2
18
+ * bug fix (thanks to Alexander Staubo for providing me with sample data);
19
+ don't fail on out-of-range IFD offsets for Apple Aperture generated JPGs
20
+
21
+ EXIF Reader 0.10.1
22
+ * old style exif access
23
+
24
+ EXIF Reader 0.10
25
+ * TIFF support
26
+
27
+ EXIF Reader 0.9.6
28
+ * bug fix; "[#8458] Conversion from string to Time fails", weird dates will now reflect nil
29
+
30
+ EXIF Reader 0.9.5.1
31
+ * make tinderbox happy by hiding rcov task
32
+
33
+ EXIF Reader 0.9.5
34
+ * patch calls to jpeg through to exif, i.e. jpeg., i.e. jpeg.model == jpeg.exif.model
35
+ * fix exifr commandline utility, needs require 'exifr' now
36
+ * improve test helper
37
+ * reduce size of test images
38
+ * include tests for tinderbox
39
+
40
+ EXIF Reader 0.9.4
41
+ * bug fix (thanks to Benjamin Storrier for providing me with sample date);
42
+ multiple app1 frames will potentially overwrite EXIF tag
43
+
44
+ EXIF Reader 0.9.3
45
+ * bug fix; "[#4876] Unable to extract gpsinfo"
46
+ * one-off bug in TiffHeader found and fixed
47
+ * make "InteroperabilityIndex" available
48
+
49
+ EXIF Reader 0.9.2
50
+ * bug fix; "[#4595] EXIFR::JPEG doesn't support multiple comments", the
51
+ comment property of a JPEG object now contains an array instead of a string
52
+ when multiple COM frames are found
53
+ * EXIF orientation modules including RMagick code to rotate to viewable state
54
+ * access to thumbnail included in EXIF
55
+ * simple commandline utility, "exifr", to view image properties
56
+ * overall code improvements including documentation and tests
57
+
58
+ EXIF Reader 0.9.1
59
+ * bug fix; "4321 Can't create object", division by zero when
60
+ denominator of rational value is zero
61
+
62
+ EXIF Reader 0.9
63
+ * 1st release
data/README.rdoc ADDED
@@ -0,0 +1,24 @@
1
+ = EXIF Reader
2
+ EXIF Reader is a module to read metadata from JPEG and TIFF images.
3
+
4
+ == Examples
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
19
+
20
+ == Author
21
+ R.W. van 't Veer
22
+
23
+ == Copyright
24
+ Copyright (c) 2006, 2007, 2008 - R.W. van 't Veer
data/Rakefile ADDED
@@ -0,0 +1,39 @@
1
+ # Copyright (c) 2006, 2007, 2008 - R.W. van 't Veer
2
+
3
+ require 'rake/rdoctask'
4
+ require 'rake/testtask'
5
+
6
+ task :default => :test
7
+
8
+ desc 'Generate site'
9
+ task :site => :rdoc do
10
+ system 'rsync -av --delete doc/ remvee@rubyforge.org:/var/www/gforge-projects/exifr'
11
+ end
12
+
13
+ Rake::RDocTask.new do |rd|
14
+ rd.title = 'EXIF Reader for Ruby API Documentation'
15
+ rd.main = "README.rdoc"
16
+ rd.rdoc_dir = "doc/api"
17
+ rd.rdoc_files.include("README.rdoc", "lib/**/*.rb")
18
+ end
19
+
20
+
21
+ Rake::TestTask.new do |t|
22
+ t.libs << 'lib' << 'tests'
23
+ t.test_files = FileList['tests/*_test.rb']
24
+ end
25
+
26
+ begin
27
+ require 'rcov/rcovtask'
28
+
29
+ Rcov::RcovTask.new do |t|
30
+ t.libs << 'lib' << 'tests'
31
+ t.test_files = FileList['tests/*_test.rb']
32
+ end
33
+
34
+ desc 'Remove all artifacts left by testing and packaging'
35
+ task :clean => [:clobber_rdoc, :clobber_rcov]
36
+ rescue LoadError
37
+ desc 'Remove all artifacts left by testing and packaging'
38
+ task :clean => [:clobber_rdoc]
39
+ end
data/bin/exifr ADDED
@@ -0,0 +1,49 @@
1
+ require 'exifr'
2
+ include EXIFR
3
+
4
+ def pp_jpeg(fname)
5
+ jpeg = JPEG.new(fname)
6
+ ks = %w(width height comment bits)
7
+ ks += jpeg.exif.to_hash.keys.map{|a|a.to_s}.sort{|a,b|a<=>b} if jpeg.exif?
8
+
9
+ l = []
10
+ ks[0..3].each do |k|
11
+ v = jpeg.send(k)
12
+ l << [k, v.inspect] if v
13
+ end
14
+ ks[4..-1].each do |k|
15
+ v = jpeg.exif.to_hash[k.to_sym]
16
+ l << [k, v.inspect] if v
17
+ end
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)
34
+ puts "#{fname}:"
35
+ f = " %#{l.sort{|a,b|a[0].size <=> b[0].size}.last[0].size}s = %s\n"
36
+ l.each{|k,v|puts f % [k, [v].flatten.map{|t|t.to_s}.join(', ')]}
37
+ end
38
+
39
+ if ARGV.size == 0
40
+ STDERR.puts "Usage: #{$0} FILE .."
41
+ else
42
+ ARGV.each do |fname|
43
+ case fname
44
+ when /\.(jpg|jpeg)$/i; pp_jpeg fname
45
+ when /\.(tif|tiff)$/i; pp_tiff fname
46
+ end
47
+ puts
48
+ end
49
+ end
data/lib/exifr.rb ADDED
@@ -0,0 +1,4 @@
1
+ # Copyright (c) 2006, 2007, 2008 - R.W. van 't Veer
2
+
3
+ require 'jpeg'
4
+ require 'tiff'
data/lib/jpeg.rb ADDED
@@ -0,0 +1,102 @@
1
+ # Copyright (c) 2006, 2007, 2008 - R.W. van 't Veer
2
+
3
+ require 'stringio'
4
+
5
+ module EXIFR
6
+ # = JPEG decoder
7
+ #
8
+ # == Examples
9
+ # EXIFR::JPEG.new('IMG_3422.JPG').width # -> 2272
10
+ # EXIFR::JPEG.new('IMG_3422.JPG').exif.model # -> "Canon PowerShot G3"
11
+ class JPEG
12
+ # image height
13
+ attr_reader :height
14
+ # image width
15
+ attr_reader :width
16
+ # number of bits per ??
17
+ attr_reader :bits # :nodoc:
18
+ # comment; a string if one comment found, an array if more,
19
+ # otherwise <tt>nil</tt>
20
+ attr_reader :comment
21
+ # EXIF data if available
22
+ attr_reader :exif
23
+
24
+ # +file+ is a filename or an IO object.
25
+ def initialize(file)
26
+ if file.kind_of? String
27
+ File.open(file, 'rb') { |io| examine(io) }
28
+ else
29
+ examine(file.dup)
30
+ end
31
+ end
32
+
33
+ # Returns +true+ when EXIF data is available.
34
+ def exif?
35
+ !exif.nil?
36
+ end
37
+
38
+ # Return thumbnail data when available.
39
+ def thumbnail
40
+ @exif && @exif.jpeg_thumbnails && @exif.jpeg_thumbnails.first
41
+ end
42
+
43
+ # Dispatch to EXIF. When no EXIF data is available but the
44
+ # +method+ does exist for EXIF data +nil+ will be returned.
45
+ def method_missing(method, *args)
46
+ super unless args.empty?
47
+ super unless TIFF::TAGS.include?(method.to_s)
48
+ @exif.send method if @exif
49
+ end
50
+
51
+ def respond_to?(method) # :nodoc:
52
+ super || TIFF::TAGS.include?(method.to_s)
53
+ end
54
+
55
+ def methods # :nodoc:
56
+ super + TIFF::TAGS
57
+ end
58
+
59
+ class << self
60
+ alias instance_methods_without_jpeg_extras instance_methods
61
+ def instance_methods(include_super = true) # :nodoc:
62
+ instance_methods_without_jpeg_extras(include_super) + TIFF::TAGS
63
+ end
64
+ end
65
+
66
+ private
67
+ def examine(io)
68
+ class << io
69
+ def readbyte; readchar; end unless method_defined?(:readbyte)
70
+ def readint; (readbyte << 8) + readbyte; end
71
+ def readframe; read(readint - 2); end
72
+ def readsof; [readint, readbyte, readint, readint, readbyte]; end
73
+ def next
74
+ c = readbyte while c != 0xFF
75
+ c = readbyte while c == 0xFF
76
+ c
77
+ end
78
+ end unless io.respond_to? :readsof
79
+
80
+ raise 'malformed JPEG' unless io.readbyte == 0xFF && io.readbyte == 0xD8 # SOI
81
+
82
+ app1s = []
83
+ while marker = io.next
84
+ case marker
85
+ when 0xC0..0xC3, 0xC5..0xC7, 0xC9..0xCB, 0xCD..0xCF # SOF markers
86
+ length, @bits, @height, @width, components = io.readsof
87
+ raise 'malformed JPEG' unless length == 8 + components * 3
88
+ when 0xD9, 0xDA; break # EOI, SOS
89
+ when 0xFE; (@comment ||= []) << io.readframe # COM
90
+ when 0xE1; app1s << io.readframe # APP1, may contain EXIF tag
91
+ else io.readframe # ignore frame
92
+ end
93
+ end
94
+
95
+ @comment = @comment.first if @comment && @comment.size == 1
96
+
97
+ if app1 = app1s.find { |d| d[0..5] == "Exif\0\0" }
98
+ @exif = TIFF.new(StringIO.new(app1[6..-1]))
99
+ end
100
+ end
101
+ end
102
+ end
data/lib/tiff.rb ADDED
@@ -0,0 +1,500 @@
1
+ # Copyright (c) 2007, 2008 - 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
+ # instances:
16
+ # * TopLeftOrientation
17
+ # * TopRightOrientation
18
+ # * BottomRightOrientation
19
+ # * BottomLeftOrientation
20
+ # * LeftTopOrientation
21
+ # * RightTopOrientation
22
+ # * RightBottomOrientation
23
+ # * LeftBottomOrientation
24
+ #
25
+ # These instances of Orientation 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::Orientation
37
+ class TIFF
38
+ include Enumerable
39
+
40
+ # JPEG thumbnails
41
+ attr_reader :jpeg_thumbnails
42
+
43
+ TAG_MAPPING = {} # :nodoc:
44
+ TAG_MAPPING.merge!({
45
+ :image => {
46
+ 0x00FE => :new_subfile_type,
47
+ 0x00FF => :subfile_type,
48
+ 0x0100 => :image_width,
49
+ 0x0101 => :image_length,
50
+ 0x0102 => :bits_per_sample,
51
+ 0x0103 => :compression,
52
+ 0x0106 => :photometric_interpretation,
53
+ 0x0107 => :threshholding,
54
+ 0x0108 => :cell_width,
55
+ 0x0109 => :cell_length,
56
+ 0x010a => :fill_order,
57
+ 0x010d => :document_name,
58
+ 0x010e => :image_description,
59
+ 0x010f => :make,
60
+ 0x0110 => :model,
61
+ 0x0111 => :strip_offsets,
62
+ 0x0112 => :orientation,
63
+ 0x0115 => :samples_per_pixel,
64
+ 0x0116 => :rows_per_strip,
65
+ 0x0117 => :strip_byte_counts,
66
+ 0x0118 => :min_sample_value,
67
+ 0x0119 => :max_sample_value,
68
+ 0x011a => :x_resolution,
69
+ 0x011b => :y_resolution,
70
+ 0x011c => :planar_configuration,
71
+ 0x011d => :page_name,
72
+ 0x011e => :x_position,
73
+ 0x011f => :y_position,
74
+ 0x0120 => :free_offsets,
75
+ 0x0121 => :free_byte_counts,
76
+ 0x0122 => :gray_response_unit,
77
+ 0x0123 => :gray_response_curve,
78
+ 0x0124 => :t4_options,
79
+ 0x0125 => :t6_options,
80
+ 0x0128 => :resolution_unit,
81
+ 0x012d => :transfer_function,
82
+ 0x0131 => :software,
83
+ 0x0132 => :date_time,
84
+ 0x013b => :artist,
85
+ 0x013c => :host_computer,
86
+ 0x013a => :predictor,
87
+ 0x013e => :white_point,
88
+ 0x013f => :primary_chromaticities,
89
+ 0x0140 => :color_map,
90
+ 0x0141 => :halftone_hints,
91
+ 0x0142 => :tile_width,
92
+ 0x0143 => :tile_length,
93
+ 0x0144 => :tile_offsets,
94
+ 0x0145 => :tile_byte_counts,
95
+ 0x0146 => :bad_fax_lines,
96
+ 0x0147 => :clean_fax_data,
97
+ 0x0148 => :consecutive_bad_fax_lines,
98
+ 0x014a => :sub_ifds,
99
+ 0x014c => :ink_set,
100
+ 0x014d => :ink_names,
101
+ 0x014e => :number_of_inks,
102
+ 0x0150 => :dot_range,
103
+ 0x0151 => :target_printer,
104
+ 0x0152 => :extra_samples,
105
+ 0x0156 => :transfer_range,
106
+ 0x0157 => :clip_path,
107
+ 0x0158 => :x_clip_path_units,
108
+ 0x0159 => :y_clip_path_units,
109
+ 0x015a => :indexed,
110
+ 0x015b => :jpeg_tables,
111
+ 0x015f => :opi_proxy,
112
+ 0x0190 => :global_parameters_ifd,
113
+ 0x0191 => :profile_type,
114
+ 0x0192 => :fax_profile,
115
+ 0x0193 => :coding_methods,
116
+ 0x0194 => :version_year,
117
+ 0x0195 => :mode_number,
118
+ 0x01B1 => :decode,
119
+ 0x01B2 => :default_image_color,
120
+ 0x0200 => :jpegproc,
121
+ 0x0201 => :jpeg_interchange_format,
122
+ 0x0202 => :jpeg_interchange_format_length,
123
+ 0x0203 => :jpeg_restart_interval,
124
+ 0x0205 => :jpeg_lossless_predictors,
125
+ 0x0206 => :jpeg_point_transforms,
126
+ 0x0207 => :jpeg_q_tables,
127
+ 0x0208 => :jpeg_dc_tables,
128
+ 0x0209 => :jpeg_ac_tables,
129
+ 0x0211 => :ycb_cr_coefficients,
130
+ 0x0212 => :ycb_cr_sub_sampling,
131
+ 0x0213 => :ycb_cr_positioning,
132
+ 0x0214 => :reference_black_white,
133
+ 0x022F => :strip_row_counts,
134
+ 0x02BC => :xmp,
135
+ 0x800D => :image_id,
136
+ 0x87AC => :image_layer,
137
+ 0x8298 => :copyright,
138
+ 0x83bb => :iptc,
139
+
140
+ 0x8769 => :exif,
141
+ 0x8825 => :gps,
142
+ },
143
+
144
+ :exif => {
145
+ 0x829a => :exposure_time,
146
+ 0x829d => :f_number,
147
+ 0x8822 => :exposure_program,
148
+ 0x8824 => :spectral_sensitivity,
149
+ 0x8827 => :iso_speed_ratings,
150
+ 0x8828 => :oecf,
151
+ 0x9000 => :exif_version,
152
+ 0x9003 => :date_time_original,
153
+ 0x9004 => :date_time_digitized,
154
+ 0x9101 => :components_configuration,
155
+ 0x9102 => :compressed_bits_per_pixel,
156
+ 0x9201 => :shutter_speed_value,
157
+ 0x9202 => :aperture_value,
158
+ 0x9203 => :brightness_value,
159
+ 0x9204 => :exposure_bias_value,
160
+ 0x9205 => :max_aperture_value,
161
+ 0x9206 => :subject_distance,
162
+ 0x9207 => :metering_mode,
163
+ 0x9208 => :light_source,
164
+ 0x9209 => :flash,
165
+ 0x920a => :focal_length,
166
+ 0x9214 => :subject_area,
167
+ 0x927c => :maker_note,
168
+ 0x9286 => :user_comment,
169
+ 0x9290 => :subsec_time,
170
+ 0x9291 => :subsec_time_orginal,
171
+ 0x9292 => :subsec_time_digitized,
172
+ 0xa000 => :flashpix_version,
173
+ 0xa001 => :color_space,
174
+ 0xa002 => :pixel_x_dimension,
175
+ 0xa003 => :pixel_y_dimension,
176
+ 0xa004 => :related_sound_file,
177
+ 0xa20b => :flash_energy,
178
+ 0xa20c => :spatial_frequency_response,
179
+ 0xa20e => :focal_plane_x_resolution,
180
+ 0xa20f => :focal_plane_y_resolution,
181
+ 0xa210 => :focal_plane_resolution_unit,
182
+ 0xa214 => :subject_location,
183
+ 0xa215 => :exposure_index,
184
+ 0xa217 => :sensing_method,
185
+ 0xa300 => :file_source,
186
+ 0xa301 => :scene_type,
187
+ 0xa302 => :cfa_pattern,
188
+ 0xa401 => :custom_rendered,
189
+ 0xa402 => :exposure_mode,
190
+ 0xa403 => :white_balance,
191
+ 0xa404 => :digital_zoom_ratio,
192
+ 0xa405 => :focal_length_in_35mm_film,
193
+ 0xa406 => :scene_capture_type,
194
+ 0xa407 => :gain_control,
195
+ 0xa408 => :contrast,
196
+ 0xa409 => :saturation,
197
+ 0xa40a => :sharpness,
198
+ 0xa40b => :device_setting_description,
199
+ 0xa40c => :subject_distance_range,
200
+ 0xa420 => :image_unique_id
201
+ },
202
+
203
+ :gps => {
204
+ 0x0000 => :gps_version_id,
205
+ 0x0001 => :gps_latitude_ref,
206
+ 0x0002 => :gps_latitude,
207
+ 0x0003 => :gps_longitude_ref,
208
+ 0x0004 => :gps_longitude,
209
+ 0x0005 => :gps_altitude_ref,
210
+ 0x0006 => :gps_altitude ,
211
+ 0x0007 => :gps_time_stamp,
212
+ 0x0008 => :gps_satellites,
213
+ 0x0009 => :gps_status,
214
+ 0x000a => :gps_measure_mode,
215
+ 0x000b => :gps_dop,
216
+ 0x000c => :gps_speed_ref,
217
+ 0x000d => :gps_speed,
218
+ 0x000e => :gps_track_ref,
219
+ 0x000f => :gps_track,
220
+ 0x0010 => :gps_img_direction_ref,
221
+ 0x0011 => :gps_img_direction,
222
+ 0x0012 => :gps_map_datum,
223
+ 0x0013 => :gps_dest_latitude_ref,
224
+ 0x0014 => :gps_dest_latitude,
225
+ 0x0015 => :gps_dest_longitude_ref,
226
+ 0x0016 => :gps_dest_longitude,
227
+ 0x0017 => :gps_dest_bearing_ref,
228
+ 0x0018 => :gps_dest_bearing,
229
+ 0x0019 => :gps_dest_distance_ref,
230
+ 0x001a => :gps_dest_distance,
231
+ 0x001b => :gps_processing_method,
232
+ 0x001c => :gps_area_information,
233
+ 0x001d => :gps_date_stamp,
234
+ 0x001e => :gps_differential,
235
+ },
236
+ })
237
+ IFD_TAGS = [:image, :exif, :gps] # :nodoc:
238
+
239
+ time_proc = proc do |value|
240
+ if value =~ /^(\d{4}):(\d\d):(\d\d) (\d\d):(\d\d):(\d\d)$/
241
+ Time.mktime($1.to_i, $2.to_i, $3.to_i, $4.to_i, $5.to_i, $6.to_i) rescue nil
242
+ else
243
+ value
244
+ end
245
+ end
246
+
247
+ # The orientation of the image with respect to the rows and columns.
248
+ class Orientation
249
+ def initialize(value, type) # :nodoc:
250
+ @value, @type = value, type
251
+ end
252
+
253
+ # Field value.
254
+ def to_i
255
+ @value
256
+ end
257
+
258
+ # Rotate and/or flip for proper viewing.
259
+ def transform_rmagick(img)
260
+ case @type
261
+ when :TopRight ; img.flop
262
+ when :BottomRight ; img.rotate(180)
263
+ when :BottomLeft ; img.flip
264
+ when :LeftTop ; img.rotate(90).flop
265
+ when :RightTop ; img.rotate(90)
266
+ when :RightBottom ; img.rotate(270).flop
267
+ when :LeftBottom ; img.rotate(270)
268
+ else
269
+ img
270
+ end
271
+ end
272
+
273
+ def ==(other) # :nodoc:
274
+ Orientation === other && to_i == other.to_i
275
+ end
276
+ end
277
+
278
+ ORIENTATIONS = [] # :nodoc:
279
+ [
280
+ nil,
281
+ :TopLeft,
282
+ :TopRight,
283
+ :BottomRight,
284
+ :BottomLeft,
285
+ :LeftTop,
286
+ :RightTop,
287
+ :RightBottom,
288
+ :LeftBottom,
289
+ ].each_with_index do |type,index|
290
+ next unless type
291
+ const_set("#{type}Orientation", ORIENTATIONS[index] = Orientation.new(index, type))
292
+ end
293
+
294
+ ADAPTERS = Hash.new { proc { |v| v } } # :nodoc:
295
+ ADAPTERS.merge!({
296
+ :date_time_original => time_proc,
297
+ :date_time_digitized => time_proc,
298
+ :date_time => time_proc,
299
+ :orientation => proc { |v| ORIENTATIONS[v] }
300
+ })
301
+
302
+ # Names for all recognized TIFF fields.
303
+ TAGS = ([TAG_MAPPING.keys, TAG_MAPPING.values.map{|v|v.values}].flatten.uniq - IFD_TAGS).map{|v|v.to_s}
304
+
305
+ # +file+ is a filename or an IO object.
306
+ def initialize(file)
307
+ data = file.respond_to?(:read) ? file.read : File.open(file, 'rb') { |io| io.read }
308
+
309
+ class << data
310
+ attr_accessor :short, :long
311
+ def readshort(pos); self[pos..(pos + 1)].unpack(@short)[0]; end
312
+ def readlong(pos); self[pos..(pos + 3)].unpack(@long)[0]; end
313
+ end
314
+
315
+ case data[0..1]
316
+ when 'II'; data.short, data.long = 'v', 'V'
317
+ when 'MM'; data.short, data.long = 'n', 'N'
318
+ else; raise 'no II or MM marker found'
319
+ end
320
+
321
+ @ifds = [IFD.new(data)]
322
+ while ifd = @ifds.last.next; @ifds << ifd; end
323
+
324
+ @jpeg_thumbnails = @ifds.map do |ifd|
325
+ if ifd.jpeg_interchange_format && ifd.jpeg_interchange_format_length
326
+ start, length = ifd.jpeg_interchange_format, ifd.jpeg_interchange_format_length
327
+ data[start..(start + length)]
328
+ end
329
+ end.compact
330
+ end
331
+
332
+ # Number of images.
333
+ def size
334
+ @ifds.size
335
+ end
336
+
337
+ # Yield for each image.
338
+ def each
339
+ @ifds.each { |ifd| yield ifd }
340
+ end
341
+
342
+ # Get +index+ image.
343
+ def [](index)
344
+ index.is_a?(Symbol) ? to_hash[index] : @ifds[index]
345
+ end
346
+
347
+ # Dispatch to first image.
348
+ def method_missing(method, *args)
349
+ super unless args.empty?
350
+
351
+ if @ifds.first.respond_to?(method)
352
+ @ifds.first.send(method)
353
+ elsif TAGS.include?(method.to_s)
354
+ @ifds.first.to_hash[method]
355
+ else
356
+ super
357
+ end
358
+ end
359
+
360
+ def respond_to?(method) # :nodoc:
361
+ super ||
362
+ (@ifds && @ifds.first && @ifds.first.respond_to?(method)) ||
363
+ TAGS.include?(method.to_s)
364
+ end
365
+
366
+ def methods # :nodoc:
367
+ (super + TAGS + IFD.instance_methods(false)).uniq
368
+ end
369
+
370
+ class << self
371
+ alias instance_methods_without_tiff_extras instance_methods
372
+ def instance_methods(include_super = true) # :nodoc:
373
+ (instance_methods_without_tiff_extras(include_super) + TAGS + IFD.instance_methods(false)).uniq
374
+ end
375
+ end
376
+
377
+ # Convenience method to access image width.
378
+ def width; @ifds.first.width; end
379
+
380
+ # Convenience method to access image height.
381
+ def height; @ifds.first.height; end
382
+
383
+ # Get a hash presentation of the (first) image.
384
+ def to_hash; @ifds.first.to_hash; end
385
+
386
+ def inspect # :nodoc:
387
+ @ifds.inspect
388
+ end
389
+
390
+ class IFD # :nodoc:
391
+ attr_reader :type, :fields
392
+
393
+ def initialize(data, offset = nil, type = :image)
394
+ @data, @offset, @type, @fields = data, offset, type, {}
395
+
396
+ pos = offset || @data.readlong(4)
397
+ num = @data.readshort(pos)
398
+ pos += 2
399
+
400
+ num.times do
401
+ add_field(Field.new(@data, pos))
402
+ pos += 12
403
+ end
404
+
405
+ @offset_next = @data.readlong(pos)
406
+ end
407
+
408
+ def method_missing(method, *args)
409
+ super unless args.empty? && TAGS.include?(method.to_s)
410
+ to_hash[method]
411
+ end
412
+
413
+ def width; image_width; end
414
+ def height; image_length; end
415
+
416
+ def to_hash
417
+ @hash ||= begin
418
+ result = @fields.dup
419
+ result.delete_if { |key,value| value.nil? }
420
+ result.each do |key,value|
421
+ if IFD_TAGS.include? key
422
+ result.merge!(value.to_hash)
423
+ result.delete key
424
+ end
425
+ end
426
+ end
427
+ end
428
+
429
+ def inspect
430
+ to_hash.inspect
431
+ end
432
+
433
+ def next?
434
+ @offset_next != 0 && @offset_next < @data.size && (@offset || 0) < @offset_next
435
+ end
436
+
437
+ def next
438
+ IFD.new(@data, @offset_next) if next?
439
+ end
440
+
441
+ def to_yaml_properties
442
+ ['@fields']
443
+ end
444
+
445
+ private
446
+ def add_field(field)
447
+ return unless tag = TAG_MAPPING[@type][field.tag]
448
+ return if @fields[tag]
449
+
450
+ if IFD_TAGS.include? tag
451
+ @fields[tag] = IFD.new(@data, field.offset, tag)
452
+ else
453
+ value = field.value.map { |v| ADAPTERS[tag][v] } if field.value
454
+ @fields[tag] = value.kind_of?(Array) && value.size == 1 ? value.first : value
455
+ end
456
+ end
457
+ end
458
+
459
+ class Field # :nodoc:
460
+ attr_reader :tag, :offset, :value
461
+
462
+ def initialize(data, pos)
463
+ @tag, count, @offset = data.readshort(pos), data.readlong(pos + 4), data.readlong(pos + 8)
464
+
465
+ case data.readshort(pos + 2)
466
+ when 1, 6 # byte, signed byte
467
+ # TODO handle signed bytes
468
+ len, pack = count, proc { |d| d }
469
+ when 2 # ascii
470
+ len, pack = count, proc { |d| d.strip }
471
+ when 3, 8 # short, signed short
472
+ # TODO handle signed
473
+ len, pack = count * 2, proc { |d| d.unpack(data.short + '*') }
474
+ when 4, 9 # long, signed long
475
+ # TODO handle signed
476
+ len, pack = count * 4, proc { |d| d.unpack(data.long + '*') }
477
+ when 5, 10
478
+ len, pack = count * 8, proc do |d|
479
+ r = []
480
+ d.unpack(data.long + '*').each_with_index do |v,i|
481
+ i % 2 == 0 ? r << [v] : r.last << v
482
+ end
483
+ r.map do |f|
484
+ if f[1] == 0 # allow NaN and Infinity
485
+ f[0].to_f.quo(f[1])
486
+ else
487
+ Rational.respond_to?(:reduce) ? Rational.reduce(*f) : f[0].quo(f[1])
488
+ end
489
+ end
490
+ end
491
+ end
492
+
493
+ if len && pack
494
+ start = len > 4 ? @offset : (pos + 8)
495
+ @value = [pack[data[start..(start + len - 1)]]].flatten
496
+ end
497
+ end
498
+ end
499
+ end
500
+ end
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
@@ -0,0 +1,84 @@
1
+ #!/usr/bin/env ruby
2
+ #
3
+ # Copyright (c) 2006, 2007, 2008 - R.W. van 't Veer
4
+
5
+ require File.join(File.dirname(__FILE__), 'test_helper')
6
+
7
+ class JPEGTest < Test::Unit::TestCase
8
+ def test_initialize
9
+ all_test_jpegs.each do |fname|
10
+ assert_nothing_raised do
11
+ JPEG.new(fname)
12
+ end
13
+ assert_nothing_raised do
14
+ open(fname) { |rd| JPEG.new(rd) }
15
+ end
16
+ assert_nothing_raised do
17
+ JPEG.new(StringIO.new(File.read(fname)))
18
+ end
19
+ end
20
+ end
21
+
22
+ def test_size
23
+ j = JPEG.new(f('image.jpg'))
24
+ assert_equal j.width, 100
25
+ assert_equal j.height, 75
26
+
27
+ j = JPEG.new(f('exif.jpg'))
28
+ assert_equal j.width, 100
29
+ assert_equal j.height, 75
30
+
31
+ j = JPEG.new(f('1x1.jpg'))
32
+ assert_equal j.width, 1
33
+ assert_equal j.height, 1
34
+ end
35
+
36
+ def test_comment
37
+ assert_equal JPEG.new(f('image.jpg')).comment, "Here's a comment!"
38
+ end
39
+
40
+ def test_exif
41
+ assert ! JPEG.new(f('image.jpg')).exif?
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
45
+ end
46
+
47
+ def test_exif_dispatch
48
+ j = JPEG.new(f('exif.jpg'))
49
+
50
+ assert JPEG.instance_methods.include?('date_time')
51
+ assert j.methods.include?('date_time')
52
+ assert j.respond_to?(:date_time)
53
+ assert j.respond_to?('date_time')
54
+ assert_not_nil j.date_time
55
+ assert_kind_of Time, j.date_time
56
+
57
+ assert_not_nil j.f_number
58
+ assert_kind_of Rational, j.f_number
59
+ end
60
+
61
+ def test_no_method_error
62
+ assert_nothing_raised { JPEG.new(f('image.jpg')).f_number }
63
+ assert_raise(NoMethodError) { JPEG.new(f('image.jpg')).foo }
64
+ end
65
+
66
+ def test_multiple_app1
67
+ assert JPEG.new(f('multiple-app1.jpg')).exif?
68
+ end
69
+
70
+ def test_thumbnail
71
+ count = 0
72
+ all_test_jpegs.each do |fname|
73
+ jpeg = JPEG.new(fname)
74
+ unless jpeg.thumbnail.nil?
75
+ assert_nothing_raised 'thumbnail not a JPEG' do
76
+ JPEG.new(StringIO.new(jpeg.thumbnail))
77
+ end
78
+ count += 1
79
+ end
80
+ end
81
+
82
+ assert count > 0, 'no thumbnails found'
83
+ end
84
+ end
@@ -0,0 +1,39 @@
1
+ #!/usr/bin/env ruby
2
+ #
3
+ # Copyright (c) 2006, 2007, 2008 - R.W. van 't Veer
4
+
5
+ require 'test/unit'
6
+ require 'stringio'
7
+ require 'pp'
8
+
9
+ $:.unshift("#{File.dirname(__FILE__)}/../lib")
10
+ require 'exifr'
11
+ include EXIFR
12
+
13
+
14
+ def all_test_jpegs
15
+ Dir[f('*.jpg')]
16
+ end
17
+
18
+
19
+ def all_test_exifs
20
+ Dir[f('*.exif')]
21
+ end
22
+
23
+ def all_test_tiffs
24
+ Dir[f('*.tif')] + all_test_exifs
25
+ end
26
+
27
+ def f(fname)
28
+ "#{File.dirname(__FILE__)}/data/#{fname}"
29
+ end
30
+
31
+ def assert_literally_equal(expected, actual, *args)
32
+ assert_equal expected.to_s, actual.to_s, *args
33
+ end
34
+
35
+ class Hash
36
+ def to_s
37
+ keys.map{|k| k.to_s}.sort.map{|k| "#{k.inspect} => #{self[k].inspect}" }.join(', ')
38
+ end
39
+ end
@@ -0,0 +1,165 @@
1
+ #!/usr/bin/env ruby
2
+ #
3
+ # Copyright (c) 2006, 2007, 2008 - R.W. van 't Veer
4
+
5
+ require File.join(File.dirname(__FILE__), 'test_helper')
6
+
7
+ class TIFFTest < 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'), f('endless-loop.exif')]).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
+ tested = 0 # count tests because not all exif samples have an orientation field
64
+ all_test_exifs.each do |fname|
65
+ orientation = TIFF.new(fname).orientation
66
+ if orientation
67
+ assert [
68
+ TIFF::TopLeftOrientation,
69
+ TIFF::TopRightOrientation,
70
+ TIFF::BottomRightOrientation,
71
+ TIFF::BottomLeftOrientation,
72
+ TIFF::LeftTopOrientation,
73
+ TIFF::RightTopOrientation,
74
+ TIFF::RightBottomOrientation,
75
+ TIFF::LeftBottomOrientation
76
+ ].any? { |c| orientation == c }, 'not an orientation'
77
+ assert orientation.respond_to?(:to_i)
78
+ assert orientation.respond_to?(:transform_rmagick)
79
+ tested += 1
80
+ end
81
+ end
82
+ assert tested > 0
83
+ end
84
+
85
+ def test_gps
86
+ t = TIFF.new(f('gps.exif'))
87
+ assert_equal "\2\2\0\0", t.gps_version_id
88
+ assert_equal 'N', t.gps_latitude_ref
89
+ assert_equal 'W', t.gps_longitude_ref
90
+ assert_equal [5355537.quo(100000), 0.quo(1), 0.quo(1)], t.gps_latitude
91
+ assert_equal [678886.quo(100000), 0.quo(1), 0.quo(1)], t.gps_longitude
92
+ assert_equal 'WGS84', t.gps_map_datum
93
+
94
+ (all_test_exifs - [f('gps.exif')]).each do |fname|
95
+ assert_nil TIFF.new(fname).gps_version_id
96
+ end
97
+ end
98
+
99
+ def test_ifd_dispatch
100
+ assert @t.respond_to?(:f_number)
101
+ assert @t.respond_to?('f_number')
102
+ assert @t.methods.include?('f_number')
103
+ assert TIFF.instance_methods.include?('f_number')
104
+
105
+ assert_not_nil @t.f_number
106
+ assert_kind_of Rational, @t.f_number
107
+ assert_not_nil @t[0].f_number
108
+ assert_kind_of Rational, @t[0].f_number
109
+ end
110
+
111
+ def test_avoid_dispatch_to_nonexistent_ifds
112
+ assert_nothing_raised do
113
+ all_test_tiffs.each do |fname|
114
+ t = TIFF.new(fname)
115
+ TIFF::TAGS.each { |tag| t.send(tag) }
116
+ end
117
+ end
118
+ end
119
+
120
+ def test_to_hash
121
+ all_test_tiffs.each do |fname|
122
+ t = TIFF.new(fname)
123
+ TIFF::TAGS.each do |key|
124
+ assert_literally_equal t.send(key), t.to_hash[key.to_sym], "#{key} not equal"
125
+ end
126
+ end
127
+ end
128
+
129
+ def test_old_style
130
+ assert_nothing_raised do
131
+ assert_not_nil @t[:f_number]
132
+ end
133
+ end
134
+
135
+ def test_yaml_dump_and_load
136
+ require 'yaml'
137
+
138
+ all_test_tiffs.each do |fname|
139
+ t = TIFF.new(fname)
140
+ y = YAML.dump(t)
141
+ assert_literally_equal t.to_hash, YAML.load(y).to_hash
142
+ end
143
+ end
144
+
145
+ def test_jpeg_thumbnails
146
+ count = 0
147
+ all_test_tiffs.each do |fname|
148
+ t = TIFF.new(fname)
149
+ unless t.jpeg_thumbnails.empty?
150
+ assert_nothing_raised do
151
+ t.jpeg_thumbnails.each do |n|
152
+ JPEG.new(StringIO.new(n))
153
+ end
154
+ end
155
+ count += 1
156
+ end
157
+ end
158
+ assert count > 0, 'no thumbnails found'
159
+ end
160
+
161
+ def test_should_not_loop_endlessly
162
+ TIFF.new(f('endless-loop.exif'))
163
+ assert true
164
+ end
165
+ end
metadata ADDED
@@ -0,0 +1,82 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: remvee-exifr
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.10.6.1
5
+ platform: ruby
6
+ authors:
7
+ - R.W. van 't Veer
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2008-07-08 00:00:00 -07:00
13
+ default_executable:
14
+ dependencies: []
15
+
16
+ description:
17
+ email: remco@remvee.net
18
+ executables:
19
+ - exifr
20
+ extensions: []
21
+
22
+ extra_rdoc_files:
23
+ - README.rdoc
24
+ - CHANGELOG
25
+ files:
26
+ - Rakefile
27
+ - bin/exifr
28
+ - lib/exifr.rb
29
+ - lib/jpeg.rb
30
+ - lib/tiff.rb
31
+ - tests/data/1x1.jpg
32
+ - tests/data/apple-aperture-1.5.exif
33
+ - tests/data/canon-g3.exif
34
+ - tests/data/Canon_PowerShot_A85.exif
35
+ - tests/data/Casio-EX-S20.exif
36
+ - tests/data/endless-loop.exif
37
+ - tests/data/exif.jpg
38
+ - tests/data/FUJIFILM-FinePix_S3000.exif
39
+ - tests/data/gps.exif
40
+ - tests/data/image.jpg
41
+ - tests/data/multiple-app1.jpg
42
+ - tests/data/nikon_d1x.tif
43
+ - tests/data/Panasonic-DMC-LC33.exif
44
+ - tests/data/plain.tif
45
+ - tests/data/Trust-DC3500_MINI.exif
46
+ - tests/data/weird_date.exif
47
+ - tests/test_helper.rb
48
+ - tests/jpeg_test.rb
49
+ - tests/tiff_test.rb
50
+ - README.rdoc
51
+ - CHANGELOG
52
+ has_rdoc: true
53
+ homepage: http://github.com/remvee/exifr/
54
+ post_install_message:
55
+ rdoc_options:
56
+ - --title
57
+ - EXIF Reader for Ruby API Documentation
58
+ - --main
59
+ - README.rdoc
60
+ require_paths:
61
+ - lib
62
+ required_ruby_version: !ruby/object:Gem::Requirement
63
+ requirements:
64
+ - - ">="
65
+ - !ruby/object:Gem::Version
66
+ version: "0"
67
+ version:
68
+ required_rubygems_version: !ruby/object:Gem::Requirement
69
+ requirements:
70
+ - - ">="
71
+ - !ruby/object:Gem::Version
72
+ version: "0"
73
+ version:
74
+ requirements: []
75
+
76
+ rubyforge_project:
77
+ rubygems_version: 1.2.0
78
+ signing_key:
79
+ specification_version: 2
80
+ summary: EXIF Reader is a module to read EXIF from JPEG images.
81
+ test_files: []
82
+