bogado-exifr 0.10.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/CHANGELOG +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
|
+
|