bogado-exifr 0.10.8

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG ADDED
@@ -0,0 +1,69 @@
1
+ EXIF Reader 0.10.8
2
+ * feature request; "[#23694] The object interface of JPEG is different from the TIFF one."
3
+
4
+ EXIF Reader 0.10.7
5
+ * bug fix; "[#22403] Wrong file size reported"
6
+
7
+ EXIF Reader 0.10.6.1
8
+ * moved to GitHub
9
+
10
+ EXIF Reader 0.10.6
11
+ * bug fix (thanks to Forian Munz for reporting it); endless loop when reading a malformed EXIF/TIFF
12
+
13
+ EXIF Reader 0.10.5
14
+ * bug fix; "[#15421] duplicate orientation field behavior", first field (of any type) is leading now
15
+ * Ruby 1.9 compatible
16
+
17
+ EXIF Reader 0.10.4
18
+ * Thumbnail extraction; [#15317] Please add thumbnail extraction
19
+
20
+ EXIF Reader 0.10.3
21
+ * YAML friendly; can now safely (de)serialize
22
+
23
+ EXIF Reader 0.10.2
24
+ * bug fix (thanks to Alexander Staubo for providing me with sample data);
25
+ don't fail on out-of-range IFD offsets for Apple Aperture generated JPGs
26
+
27
+ EXIF Reader 0.10.1
28
+ * old style exif access
29
+
30
+ EXIF Reader 0.10
31
+ * TIFF support
32
+
33
+ EXIF Reader 0.9.6
34
+ * bug fix; "[#8458] Conversion from string to Time fails", weird dates will now reflect nil
35
+
36
+ EXIF Reader 0.9.5.1
37
+ * make tinderbox happy by hiding rcov task
38
+
39
+ EXIF Reader 0.9.5
40
+ * patch calls to jpeg through to exif, i.e. jpeg., i.e. jpeg.model == jpeg.exif.model
41
+ * fix exifr commandline utility, needs require 'exifr' now
42
+ * improve test helper
43
+ * reduce size of test images
44
+ * include tests for tinderbox
45
+
46
+ EXIF Reader 0.9.4
47
+ * bug fix (thanks to Benjamin Storrier for providing me with sample date);
48
+ multiple app1 frames will potentially overwrite EXIF tag
49
+
50
+ EXIF Reader 0.9.3
51
+ * bug fix; "[#4876] Unable to extract gpsinfo"
52
+ * one-off bug in TiffHeader found and fixed
53
+ * make "InteroperabilityIndex" available
54
+
55
+ EXIF Reader 0.9.2
56
+ * bug fix; "[#4595] EXIFR::JPEG doesn't support multiple comments", the
57
+ comment property of a JPEG object now contains an array instead of a string
58
+ when multiple COM frames are found
59
+ * EXIF orientation modules including RMagick code to rotate to viewable state
60
+ * access to thumbnail included in EXIF
61
+ * simple commandline utility, "exifr", to view image properties
62
+ * overall code improvements including documentation and tests
63
+
64
+ EXIF Reader 0.9.1
65
+ * bug fix; "4321 Can't create object", division by zero when
66
+ denominator of rational value is zero
67
+
68
+ EXIF Reader 0.9
69
+ * 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, 2009 - R.W. van 't Veer
data/Rakefile ADDED
@@ -0,0 +1,39 @@
1
+ # Copyright (c) 2006, 2007, 2008, 2009 - 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, 2009 - R.W. van 't Veer
2
+
3
+ require 'jpeg'
4
+ require 'tiff'
data/lib/jpeg.rb ADDED
@@ -0,0 +1,109 @@
1
+ # Copyright (c) 2006, 2007, 2008, 2009 - 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
+ # Get a hash presentation of the image.
44
+ def to_hash
45
+ h = {:width => width, :height => height, :bits => bits, :comment => comment}
46
+ h.merge!(exif) if exif?
47
+ h
48
+ end
49
+
50
+ # Dispatch to EXIF. When no EXIF data is available but the
51
+ # +method+ does exist for EXIF data +nil+ will be returned.
52
+ def method_missing(method, *args)
53
+ super unless args.empty?
54
+ super unless TIFF::TAGS.include?(method.to_s)
55
+ @exif.send method if @exif
56
+ end
57
+
58
+ def respond_to?(method) # :nodoc:
59
+ super || TIFF::TAGS.include?(method.to_s)
60
+ end
61
+
62
+ def methods # :nodoc:
63
+ super + TIFF::TAGS
64
+ end
65
+
66
+ class << self
67
+ alias instance_methods_without_jpeg_extras instance_methods
68
+ def instance_methods(include_super = true) # :nodoc:
69
+ instance_methods_without_jpeg_extras(include_super) + TIFF::TAGS
70
+ end
71
+ end
72
+
73
+ private
74
+ def examine(io)
75
+ class << io
76
+ def readbyte; readchar; end unless method_defined?(:readbyte)
77
+ def readint; (readbyte << 8) + readbyte; end
78
+ def readframe; read(readint - 2); end
79
+ def readsof; [readint, readbyte, readint, readint, readbyte]; end
80
+ def next
81
+ c = readbyte while c != 0xFF
82
+ c = readbyte while c == 0xFF
83
+ c
84
+ end
85
+ end unless io.respond_to? :readsof
86
+
87
+ raise 'malformed JPEG' unless io.readbyte == 0xFF && io.readbyte == 0xD8 # SOI
88
+
89
+ app1s = []
90
+ while marker = io.next
91
+ case marker
92
+ when 0xC0..0xC3, 0xC5..0xC7, 0xC9..0xCB, 0xCD..0xCF # SOF markers
93
+ length, @bits, @height, @width, components = io.readsof
94
+ raise 'malformed JPEG' unless length == 8 + components * 3
95
+ when 0xD9, 0xDA; break # EOI, SOS
96
+ when 0xFE; (@comment ||= []) << io.readframe # COM
97
+ when 0xE1; app1s << io.readframe # APP1, may contain EXIF tag
98
+ else io.readframe # ignore frame
99
+ end
100
+ end
101
+
102
+ @comment = @comment.first if @comment && @comment.size == 1
103
+
104
+ if app1 = app1s.find { |d| d[0..5] == "Exif\0\0" }
105
+ @exif = TIFF.new(StringIO.new(app1[6..-1]))
106
+ end
107
+ end
108
+ end
109
+ end
data/lib/tiff.rb ADDED
@@ -0,0 +1,553 @@
1
+ # Copyright (c) 2007, 2008, 2009 - 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 = Data.new(file)
308
+
309
+ case data[0..1]
310
+ when 'II'; data.endianess = 'v'
311
+ when 'MM'; data.endianess = 'n'
312
+ else; raise 'no II or MM marker found'
313
+ end
314
+
315
+ @ifds = [IFD.new(data)]
316
+ while ifd = @ifds.last.next
317
+ break if @ifds.find{|i| i.offset == ifd.offset}
318
+ @ifds << ifd
319
+ end
320
+
321
+ @jpeg_thumbnails = @ifds.map do |ifd|
322
+ if ifd.jpeg_interchange_format && ifd.jpeg_interchange_format_length
323
+ start, length = ifd.jpeg_interchange_format, ifd.jpeg_interchange_format_length
324
+ data[start..(start + length)]
325
+ end
326
+ end.compact
327
+ end
328
+
329
+ # Number of images.
330
+ def size
331
+ @ifds.size
332
+ end
333
+
334
+ # Yield for each image.
335
+ def each
336
+ @ifds.each { |ifd| yield ifd }
337
+ end
338
+
339
+ # Get +index+ image.
340
+ def [](index)
341
+ index.is_a?(Symbol) ? to_hash[index] : @ifds[index]
342
+ end
343
+
344
+ # Dispatch to first image.
345
+ def method_missing(method, *args)
346
+ super unless args.empty?
347
+
348
+ if @ifds.first.respond_to?(method)
349
+ @ifds.first.send(method)
350
+ elsif TAGS.include?(method.to_s)
351
+ @ifds.first.to_hash[method]
352
+ else
353
+ super
354
+ end
355
+ end
356
+
357
+ def respond_to?(method) # :nodoc:
358
+ super ||
359
+ (@ifds && @ifds.first && @ifds.first.respond_to?(method)) ||
360
+ TAGS.include?(method.to_s)
361
+ end
362
+
363
+ def methods # :nodoc:
364
+ (super + TAGS + IFD.instance_methods(false)).uniq
365
+ end
366
+
367
+ class << self
368
+ alias instance_methods_without_tiff_extras instance_methods
369
+ def instance_methods(include_super = true) # :nodoc:
370
+ (instance_methods_without_tiff_extras(include_super) + TAGS + IFD.instance_methods(false)).uniq
371
+ end
372
+ end
373
+
374
+ # Convenience method to access image width.
375
+ def width; @ifds.first.width; end
376
+
377
+ # Convenience method to access image height.
378
+ def height; @ifds.first.height; end
379
+
380
+ # Get a hash presentation of the (first) image.
381
+ def to_hash; @ifds.first.to_hash; end
382
+
383
+ def inspect # :nodoc:
384
+ @ifds.inspect
385
+ end
386
+
387
+ class IFD # :nodoc:
388
+ attr_reader :type, :fields, :offset
389
+
390
+ def initialize(data, offset = nil, type = :image)
391
+ @data, @offset, @type, @fields = data, offset, type, {}
392
+
393
+ pos = offset || @data.readlong(4)
394
+ num = @data.readshort(pos)
395
+ pos += 2
396
+
397
+ num.times do
398
+ add_field(Field.new(@data, pos))
399
+ pos += 12
400
+ end
401
+
402
+ @offset_next = @data.readlong(pos)
403
+ end
404
+
405
+ def method_missing(method, *args)
406
+ super unless args.empty? && TAGS.include?(method.to_s)
407
+ to_hash[method]
408
+ end
409
+
410
+ def width; image_width; end
411
+ def height; image_length; end
412
+
413
+ def to_hash
414
+ @hash ||= begin
415
+ result = @fields.dup
416
+ result.delete_if { |key,value| value.nil? }
417
+ result.each do |key,value|
418
+ if IFD_TAGS.include? key
419
+ result.merge!(value.to_hash)
420
+ result.delete key
421
+ end
422
+ end
423
+ end
424
+ end
425
+
426
+ def inspect
427
+ to_hash.inspect
428
+ end
429
+
430
+ def next?
431
+ @offset_next != 0 && @offset_next < @data.size
432
+ end
433
+
434
+ def next
435
+ IFD.new(@data, @offset_next) if next?
436
+ end
437
+
438
+ def to_yaml_properties
439
+ ['@fields']
440
+ end
441
+
442
+ private
443
+ def add_field(field)
444
+ return unless tag = TAG_MAPPING[@type][field.tag]
445
+ return if @fields[tag]
446
+
447
+ if IFD_TAGS.include? tag
448
+ @fields[tag] = IFD.new(@data, field.offset, tag)
449
+ else
450
+ value = field.value.map { |v| ADAPTERS[tag][v] } if field.value
451
+ @fields[tag] = value.kind_of?(Array) && value.size == 1 ? value.first : value
452
+ end
453
+ end
454
+ end
455
+
456
+ class Field # :nodoc:
457
+ attr_reader :tag, :offset, :value
458
+
459
+ def initialize(data, pos)
460
+ @tag, count, @offset = data.readshort(pos), data.readlong(pos + 4), data.readlong(pos + 8)
461
+
462
+ case data.readshort(pos + 2)
463
+ when 1, 6 # byte, signed byte
464
+ # TODO handle signed bytes
465
+ len, pack = count, proc { |d| d }
466
+ when 2 # ascii
467
+ len, pack = count, proc { |d| d.strip }
468
+ when 3, 8 # short, signed short
469
+ # TODO handle signed
470
+ len, pack = count * 2, proc { |d| d.unpack(data.short + '*') }
471
+ when 4, 9 # long, signed long
472
+ # TODO handle signed
473
+ len, pack = count * 4, proc { |d| d.unpack(data.long + '*') }
474
+ when 5, 10
475
+ len, pack = count * 8, proc do |d|
476
+ r = []
477
+ d.unpack(data.long + '*').each_with_index do |v,i|
478
+ i % 2 == 0 ? r << [v] : r.last << v
479
+ end
480
+ r.map do |f|
481
+ if f[1] == 0 # allow NaN and Infinity
482
+ f[0].to_f.quo(f[1])
483
+ else
484
+ Rational.respond_to?(:reduce) ? Rational.reduce(*f) : f[0].quo(f[1])
485
+ end
486
+ end
487
+ end
488
+ end
489
+
490
+ if len && pack
491
+ start = len > 4 ? @offset : (pos + 8)
492
+ @value = [pack[data[start..(start + len - 1)]]].flatten
493
+ end
494
+ end
495
+ end
496
+
497
+ class Data
498
+ attr_reader :short, :long
499
+
500
+ def initialize(file)
501
+ @file = file.respond_to?(:read) ? file : File.open(file, "rb")
502
+ @buff = []
503
+ @pos = 0
504
+ @size = 0
505
+ end
506
+
507
+ def endianess=(endianess)
508
+ @short = endianess.downcase
509
+ @long = endianess.upcase
510
+ end
511
+
512
+ def [](pos)
513
+ # handle Ranges
514
+ if (pos.respond_to?(:min) and pos.respond_to?(:max))
515
+ min = pos.min
516
+ max = pos.max
517
+ else
518
+ min = pos
519
+ max = pos
520
+ end
521
+
522
+ if (min < @pos or max >= @pos + @size)
523
+ buff_read(min, max - min)
524
+ end
525
+
526
+ return @buffer[(min - @pos)..(max - @pos)]
527
+ end
528
+
529
+ def readshort(pos)
530
+ self[pos..(pos + 1)].unpack(@short)[0]
531
+ end
532
+
533
+ def readlong(pos)
534
+ self[pos..(pos + 3)].unpack(@long)[0]
535
+ end
536
+
537
+ def size
538
+ @file.seek(0, IO::SEEK_END)
539
+ return @file.pos
540
+ end
541
+
542
+ private
543
+ def buff_read(pos, size)
544
+ @pos = pos
545
+ @size = size < 4096? 4096 : size;
546
+ @file.seek(pos)
547
+ @buffer = @file.read(@size)
548
+ # read can read less then the requested size
549
+ @size = @buffer.size
550
+ end
551
+ end
552
+ end
553
+ 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,96 @@
1
+ #!/usr/bin/env ruby
2
+ #
3
+ # Copyright (c) 2006, 2007, 2008, 2009 - 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_to_hash
48
+ h = JPEG.new(f('image.jpg')).to_hash
49
+ assert_equal 100, h[:width]
50
+ assert_equal 75, h[:height]
51
+ assert_equal "Here's a comment!", h[:comment]
52
+
53
+ h = JPEG.new(f('exif.jpg')).to_hash
54
+ assert_equal 100, h[:width]
55
+ assert_equal 75, h[:height]
56
+ assert_kind_of Time, h[:date_time]
57
+ end
58
+
59
+ def test_exif_dispatch
60
+ j = JPEG.new(f('exif.jpg'))
61
+
62
+ assert JPEG.instance_methods.include?('date_time')
63
+ assert j.methods.include?('date_time')
64
+ assert j.respond_to?(:date_time)
65
+ assert j.respond_to?('date_time')
66
+ assert_not_nil j.date_time
67
+ assert_kind_of Time, j.date_time
68
+
69
+ assert_not_nil j.f_number
70
+ assert_kind_of Rational, j.f_number
71
+ end
72
+
73
+ def test_no_method_error
74
+ assert_nothing_raised { JPEG.new(f('image.jpg')).f_number }
75
+ assert_raise(NoMethodError) { JPEG.new(f('image.jpg')).foo }
76
+ end
77
+
78
+ def test_multiple_app1
79
+ assert JPEG.new(f('multiple-app1.jpg')).exif?
80
+ end
81
+
82
+ def test_thumbnail
83
+ count = 0
84
+ all_test_jpegs.each do |fname|
85
+ jpeg = JPEG.new(fname)
86
+ unless jpeg.thumbnail.nil?
87
+ assert_nothing_raised 'thumbnail not a JPEG' do
88
+ JPEG.new(StringIO.new(jpeg.thumbnail))
89
+ end
90
+ count += 1
91
+ end
92
+ end
93
+
94
+ assert count > 0, 'no thumbnails found'
95
+ end
96
+ end
@@ -0,0 +1,39 @@
1
+ #!/usr/bin/env ruby
2
+ #
3
+ # Copyright (c) 2006, 2007, 2008, 2009 - 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, 2009 - 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: bogado-exifr
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.10.8
5
+ platform: ruby
6
+ authors:
7
+ - R.W. van 't Veer
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-02-13 00:00:00 -08: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
+