bogado-exifr 0.10.8
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +69 -0
- data/README.rdoc +24 -0
- data/Rakefile +39 -0
- data/bin/exifr +49 -0
- data/lib/exifr.rb +4 -0
- data/lib/jpeg.rb +109 -0
- data/lib/tiff.rb +553 -0
- data/tests/data/1x1.jpg +0 -0
- data/tests/data/Canon_PowerShot_A85.exif +0 -0
- data/tests/data/Casio-EX-S20.exif +0 -0
- data/tests/data/FUJIFILM-FinePix_S3000.exif +0 -0
- data/tests/data/Panasonic-DMC-LC33.exif +0 -0
- data/tests/data/Trust-DC3500_MINI.exif +0 -0
- data/tests/data/apple-aperture-1.5.exif +0 -0
- data/tests/data/canon-g3.exif +0 -0
- data/tests/data/endless-loop.exif +0 -0
- data/tests/data/exif.jpg +0 -0
- data/tests/data/gps.exif +0 -0
- data/tests/data/image.jpg +0 -0
- data/tests/data/multiple-app1.jpg +0 -0
- data/tests/data/nikon_d1x.tif +0 -0
- data/tests/data/plain.tif +0 -0
- data/tests/data/weird_date.exif +0 -0
- data/tests/jpeg_test.rb +96 -0
- data/tests/test_helper.rb +39 -0
- data/tests/tiff_test.rb +165 -0
- metadata +82 -0
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
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
|
data/tests/data/1x1.jpg
ADDED
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
data/tests/data/exif.jpg
ADDED
Binary file
|
data/tests/data/gps.exif
ADDED
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
data/tests/jpeg_test.rb
ADDED
@@ -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
|
data/tests/tiff_test.rb
ADDED
@@ -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
|
+
|