cul_image_props 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,189 @@
1
+ require 'cul_image_props/image/magic'
2
+ require 'cul_image_props/image/properties/exif/types'
3
+ require 'cul_image_props/image/properties/exif/constants'
4
+ module Cul
5
+ module Image
6
+ module Properties
7
+ module Exif
8
+
9
+
10
+ # process an image file (expects an open file object)
11
+ # this is the function that has to deal with all the arbitrary nasty bits
12
+ # of the EXIF standard
13
+ def self.process_file(f, opts={})
14
+ def_opts = { :stop_tag => 'UNDEF', :details => true, :strict => false }
15
+ opts = def_opts.merge(opts)
16
+ stop_tag = opts[:stop_tag]
17
+ details = opts[:details]
18
+ strict = opts[:strict]
19
+
20
+ # by default do not fake an EXIF beginning
21
+ fake_exif = 0
22
+
23
+ # determine whether it's a JPEG or TIFF
24
+ data = f.read(12)
25
+ jfif = {}
26
+ if [Cul::Image::Magic::TIFF_MOTOROLA_BE, Cul::Image::Magic::TIFF_INTEL_LE].include? data[0, 4]
27
+ f.seek(0)
28
+ endian = data[0,1]
29
+ offset = 0
30
+ elsif data[0,2] == Cul::Image::Magic::JPEG
31
+ # it's a JPEG file
32
+ base = 0
33
+ while data[2] == "\xFF" and ['JFIF', 'JFXX', 'OLYM', 'Phot'].include? data[6, 4]
34
+ length = data[4,2].unpack('n')[0]
35
+ f.read(length-8)
36
+ # fake an EXIF beginning of file
37
+ data = "\xFF\x00"+f.read(10)
38
+ fake_exif = 1
39
+ base = base + length
40
+ end
41
+ # Big ugly patch to deal with APP2 (or other) data coming before APP1
42
+ f.seek(0)
43
+ data = f.read(base+8000) # in theory, this could be insufficient --gd
44
+
45
+ base = 2
46
+ fptr = base
47
+ while true
48
+ if data[fptr,2]=="\xFF\xE1"
49
+ if data[fptr+4,4] == "Exif"
50
+ base = fptr-2
51
+ break
52
+ end
53
+ fptr += 2
54
+ fptr += data[fptr+2,2].unpack('n')[0]
55
+ elsif data[fptr,2]=="\xFF\xE2"
56
+ fptr += 2
57
+ fptr += data[fptr+2,2].unpack('n')[0]
58
+ elsif data[fptr,2]=="\xFF\xE0"
59
+ offset = fptr
60
+ fptr += 2
61
+ fptr += data[fptr,2].unpack('n')[0]
62
+
63
+ exif = EXIF_TAGS[0x0128]
64
+ label = 'Image ' + exif.name
65
+ if (data[offset+11] == 0x00)
66
+ jfif[label] = IFD_Tag.new(exif.value[1],label,3,[0],offset+11,1)
67
+ elsif (data[offset+11] == 0x01)
68
+ jfif[label] = IFD_Tag.new(exif.value[2],label,3,[0],offset+11,1)
69
+ elsif (data[offset+11] == 0x02)
70
+ jfif[label] = IFD_Tag.new(exif.value[3],label,3,[0],offset+11,1)
71
+ else
72
+ jfif[label] = IFD_Tag.new("Unknown",label,3,[0],offset+11,1)
73
+ end
74
+ xres= data[offset+12,2].unpack('n')[0]
75
+ yres= data[offset+14,2].unpack('n')[0]
76
+ exif = EXIF_TAGS[0x011a]
77
+ label = 'Image ' + EXIF_TAGS[0x011a].name
78
+ jfif[label] = IFD_Tag.new(xres.to_s,label,5,[xres],offset+12,2)
79
+ label = 'Image ' + EXIF_TAGS[0x011b].name
80
+ jfif[label] = IFD_Tag.new(yres.to_s,label,5,[yres],offset+13,2)
81
+ else
82
+ if(data.length < fptr + 2)
83
+ break
84
+ end
85
+ # scan for the next APP header, /\xFF(^[\x00])/
86
+ _next = data.index("\xff",fptr + 2)
87
+ _next = nil if !_next.nil? and _next + 2 >= data.length
88
+ while (!_next.nil? and data[_next + 1] == 0x00)
89
+ _next = data.index("\xff",_next + 2)
90
+ _next = nil if !_next.nil? and _next + 2 >= data.length
91
+ end
92
+ unless (_next.nil?)
93
+ fptr = _next
94
+ else
95
+ break
96
+ end
97
+ end
98
+ end
99
+ f.seek(base+12)
100
+ if data[2+base] == 0xFF and data[6+base, 4] == 'Exif'
101
+ # detected EXIF header
102
+ offset = base+12 # f.tell()
103
+ endian = f.read(1)
104
+ else
105
+ # no EXIF information
106
+ unless fake_exif
107
+ return {}
108
+ else
109
+ return jfif
110
+ end
111
+ end
112
+ else
113
+ # file format not recognized
114
+ return {}
115
+ end
116
+ # deal with the EXIF info we found
117
+
118
+ hdr = EXIF_header.new(f, endian, offset, fake_exif, strict)
119
+ jfif.each { |tag|
120
+ unless hdr.tags.include? tag
121
+ hdr.tags[tag] = jfif[tag]
122
+ end
123
+ }
124
+ ifd_list = hdr.list_IFDs()
125
+ ctr = 0
126
+ ifd_list.each { |i|
127
+ if ctr == 0
128
+ ifd_name = 'Image'
129
+ elsif ctr == 1
130
+ ifd_name = 'Thumbnail'
131
+ thumb_ifd = i
132
+ else
133
+ ifd_name = 'IFD %d' % ctr
134
+ end
135
+ hdr.dump_IFD(i, ifd_name, {:dict=>EXIF_TAGS, :relative=>false, :stop_tag=>stop_tag})
136
+ # EXIF IFD
137
+ exif_off = hdr.tags[ifd_name +' ExifOffset']
138
+ if exif_off
139
+ hdr.dump_IFD(exif_off.values[0], 'EXIF', {:dict=>EXIF_TAGS, :relative=>false, :stop_tag =>stop_tag})
140
+ # Interoperability IFD contained in EXIF IFD
141
+ intr_off = hdr.tags['EXIF SubIFD InteroperabilityOffset']
142
+ if intr_off
143
+ hdr.dump_IFD(intr_off.values[0], 'EXIF Interoperability',
144
+ :dict=>INTR_TAGS, :relative=>false, :stop_tag =>stop_tag)
145
+ end
146
+ end
147
+ # GPS IFD
148
+ gps_off = hdr.tags[ifd_name+' GPSInfo']
149
+ if gps_off
150
+ hdr.dump_IFD(gps_off.values[0], 'GPS', {:dict=>GPS_TAGS, :relative=>false, :stop_tag =>stop_tag})
151
+ end
152
+ ctr += 1
153
+ }
154
+ # extract uncompressed TIFF thumbnail
155
+ thumb = hdr.tags['Thumbnail Compression']
156
+ if thumb and thumb.printable == 'Uncompressed TIFF'
157
+ hdr.extract_TIFF_thumbnail(thumb_ifd)
158
+ end
159
+ # JPEG thumbnail (thankfully the JPEG data is stored as a unit)
160
+ thumb_off = hdr.tags['Thumbnail JPEGInterchangeFormat']
161
+ if thumb_off
162
+ f.seek(offset+thumb_off.values[0])
163
+ size = hdr.tags['Thumbnail JPEGInterchangeFormatLength'].values[0]
164
+ hdr.tags['JPEGThumbnail'] = f.read(size)
165
+ end
166
+
167
+ # deal with MakerNote contained in EXIF IFD
168
+ # (Some apps use MakerNote tags but do not use a format for which we
169
+ # have a description, do not process these).
170
+ if hdr.tags.include? 'EXIF MakerNote' and hdr.tags.include? 'Image Make' and detailed
171
+ hdr.decode_maker_note()
172
+ end
173
+
174
+ # Sometimes in a TIFF file, a JPEG thumbnail is hidden in the MakerNote
175
+ # since it's not allowed in a uncompressed TIFF IFD
176
+ unless hdr.tags.include? 'JPEGThumbnail'
177
+ thumb_off=hdr.tags['MakerNote JPEGThumbnail']
178
+ if thumb_off
179
+ f.seek(offset+thumb_off.values[0])
180
+ hdr.tags['JPEGThumbnail']=file.read(thumb_off.field_length)
181
+ end
182
+ end
183
+ return hdr.tags
184
+ end
185
+
186
+ end
187
+ end
188
+ end
189
+ end
@@ -0,0 +1,318 @@
1
+ require 'nokogiri'
2
+ require 'cul_image_props/image/properties/exif'
3
+ module Cul
4
+ module Image
5
+ module Properties
6
+
7
+ class Namespace
8
+ def initialize(href, prefix)
9
+ @href = href
10
+ @prefix = prefix
11
+ end
12
+ def href
13
+ @href
14
+ end
15
+ def prefix
16
+ @prefix
17
+ end
18
+ end
19
+
20
+ ASSESS = Namespace.new("http://purl.oclc.org/NET/CUL/RESOURCE/STILLIMAGE/ASSESSMENT/","si-assess")
21
+ BASIC = Namespace.new("http://purl.oclc.org/NET/CUL/RESOURCE/STILLIMAGE/BASIC/","si-basic")
22
+ DCMI = Namespace.new("http://purl.org/dc/terms/","dcmi")
23
+
24
+ class Base
25
+ attr_accessor :nodeset
26
+ BASE_XML = Nokogiri::XML.parse(<<-xml
27
+ <rdf:Description xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
28
+ xmlns:si-assess="http://purl.oclc.org/NET/CUL/RESOURCE/STILLIMAGE/ASSESSMENT/"
29
+ xmlns:si-basic="http://purl.oclc.org/NET/CUL/RESOURCE/STILLIMAGE/BASIC/"
30
+ xmlns:dcmi="http://purl.org/dc/terms/"></rdf:Description>
31
+ xml
32
+ )
33
+ def initialize(srcfile=nil)
34
+ @src = srcfile
35
+ @src.rewind
36
+ @ng_xml = BASE_XML.clone
37
+ end
38
+ def nodeset
39
+ @ng_xml.root.element_children
40
+ end
41
+ def [](key)
42
+ result = nil
43
+ nodeset.each { |node|
44
+ if (node.namespace.href + node.name) == key
45
+ if node.attribute('resource').nil?
46
+ result = node.text
47
+ else
48
+ result = node.attribute('resource').value
49
+ end
50
+ end
51
+ }
52
+ result
53
+ end
54
+ def add_dt_prop(prefix, name, value)
55
+ prop = @ng_xml.create_element(name)
56
+ @ng_xml.root.namespace_definitions.each { |ns| prop.namespace = ns if ns.prefix == prefix }
57
+ prop.add_child(@ng_xml.create_text_node(value.to_s))
58
+ @ng_xml.root.add_child( prop )
59
+ end
60
+
61
+ def sampling_unit=(name)
62
+ prop = @ng_xml.create_element("samplingFrequencyUnit")
63
+ @ng_xml.root.namespace_definitions.each { |ns| prop.namespace = ns if ns.prefix == "si-assess" }
64
+ @ng_xml.root.add_child( prop )
65
+ prop.set_attribute("rdf:resource", prop.namespace.href + name)
66
+ end
67
+
68
+ def x_sampling_freq=(value)
69
+ add_dt_prop("si-assess", "xSamplingFrequency", value)
70
+ end
71
+
72
+ def y_sampling_freq=(value)
73
+ add_dt_prop("si-assess", "ySamplingFrequency", value)
74
+ end
75
+
76
+ def width=(value)
77
+ add_dt_prop("si-basic", "imageWidth", value)
78
+ end
79
+
80
+ def length=(value)
81
+ add_dt_prop("si-basic", "imageLength", value)
82
+ end
83
+
84
+ def extent=(value)
85
+ add_dt_prop("dcmi", "extent", value)
86
+ end
87
+ end
88
+
89
+ class Bmp < Base
90
+ def initialize(srcfile=nil)
91
+ super
92
+ header_bytes = @src.read(18)
93
+ raise "Source file is not a bitmap" unless header_bytes[0...2] == Cul::Image::Magic::BMP
94
+ size = header_bytes[-4,4].unpack('V')[0]
95
+ header_bytes = header_bytes + @src.read(size)
96
+ dims = header_bytes[0x12...0x1a].unpack('VV')
97
+ sampling = header_bytes[0x26...0x2e].unpack('VV')
98
+ self.sampling_unit='CentimeterSampling'
99
+ self.width= dims[0]
100
+ self.length= dims[1]
101
+ self.extent= srcfile.stat.size unless srcfile.nil?
102
+ self.x_sampling_freq= (sampling[0]) # / 100).ceil
103
+ self.y_sampling_freq= (sampling[1]) # / 100).ceil
104
+ end
105
+ end
106
+
107
+ class Gif < Base
108
+ def initialize(srcfile=nil)
109
+ super
110
+ header_bytes = @src.read(13)
111
+ raise "Source file is not a gif" unless header_bytes[0...4] == Cul::Image::Magic::GIF
112
+ self.width= header_bytes[6,2].unpack('v')[0]
113
+ self.length= header_bytes[8,2].unpack('v')[0]
114
+ self.extent= srcfile.stat.size unless srcfile.nil?
115
+ end
116
+ end
117
+
118
+ class Jpeg < Base
119
+ def initialize(srcfile=nil)
120
+ super
121
+ header_bytes = @src.read(2)
122
+ raise "Source file is not a jpeg" unless header_bytes[0...2] == Cul::Image::Magic::JPEG
123
+ xpix = 0
124
+ ypix = 0
125
+ while (!@src.eof?)
126
+ if "\xFF" == @src.read(1)
127
+ mrkr = "\xFF" + @src.read(1)
128
+ blen = @src.read(2).unpack('n')[0]
129
+ if Cul::Image::Magic::JPEG_FRAME_MARKERS.include? mrkr # SOFn, Start of frame for scans
130
+ @src.read(1) #skip bits per sample
131
+ self.length= @src.read(2).unpack('n')[0]
132
+ self.width= @src.read(2).unpack('n')[0]
133
+ @src.seek(0, IO::SEEK_END)
134
+ else
135
+ @src.seek(blen - 2, IO::SEEK_CUR)
136
+ end
137
+ else
138
+ @src.seek(0, IO::SEEK_END)
139
+ end
140
+ end
141
+
142
+ @src.rewind
143
+ tags = Cul::Image::Properties::Exif.process_file(@src)
144
+ if tags.include? 'Image ImageWidth'
145
+ self.width= tags['Image ImageWidth'].values[0]
146
+ end
147
+ if tags.include? 'Image ImageLength'
148
+ self.length= tags['Image ImageLength'].values[0]
149
+ end
150
+ if tags.include? 'Image XResolution'
151
+ self.x_sampling_freq= tags['Image XResolution'].values[0]
152
+ end
153
+ if tags.include? 'Image YResolution'
154
+ self.y_sampling_freq= tags['Image YResolution'].values[0]
155
+ end
156
+ if tags.include? 'Image ResolutionUnit'
157
+ if (tags['Image ResolutionUnit'].values[0] == 3)
158
+ self.sampling_unit='CentimeterSampling'
159
+ elsif (tags['Image ResolutionUnit'].values[0] == 2)
160
+ self.sampling_unit='InchSampling'
161
+ else
162
+ self.sampling_unit='NoAbsoluteSampling'
163
+ end
164
+ end
165
+ self.extent= srcfile.stat.size unless srcfile.nil?
166
+ end
167
+ end
168
+
169
+ class Png < Base
170
+ def initialize(srcfile=nil)
171
+ super
172
+ header_bytes = @src.read(8)
173
+ raise "Source file is not a png" unless header_bytes[0...8] == Cul::Image::Magic::PNG
174
+ until @src.eof?
175
+ clen = @src.read(4).unpack('N')[0]
176
+ ctype = @src.read(4)
177
+ case ctype
178
+ when 'pHYs'
179
+ pHYs(clen)
180
+ when 'IHDR'
181
+ IHDR(clen)
182
+ when 'tEXt'
183
+ tEXt(clen)
184
+ when 'IEND'
185
+ IEND(clen)
186
+ else
187
+ @src.seek(clen+4, IO::SEEK_CUR)
188
+ end
189
+ end
190
+ self.extent= srcfile.stat.size unless srcfile.nil?
191
+ end
192
+ def pHYs(len)
193
+ val = @src.read(9)
194
+ xres = val[0,4].unpack('N')[0]
195
+ yres = val[4,4].unpack('N')[0]
196
+ unit = val[8]
197
+ if unit == 1 # resolution unit is METER
198
+ xres = (xres / 100).ceil
199
+ yres = (yres / 100).ceil
200
+ self.sampling_unit='CentimeterSampling'
201
+ else
202
+ self.sampling_unit='NoAbsoluteSampling'
203
+ end
204
+ self.x_sampling_freq= xres
205
+ self.y_sampling_freq= yres
206
+ @src.seek(len - 5, IO::SEEK_CUR) # remaining block + end tag
207
+ end
208
+ def IHDR(len)
209
+ val = @src.read(8)
210
+ self.width= val[0,4].unpack('N')[0]
211
+ self.length= val[4,4].unpack('N')[0]
212
+ @src.seek(len - 4, IO::SEEK_CUR) # remaining block + end tag
213
+ end
214
+ def tEXt(len)
215
+ @src.seek(len + 4, IO::SEEK_CUR) # remaining block + end tag
216
+ end
217
+ def IEND(len)
218
+ @src.seek(0, IO::SEEK_END)
219
+ end
220
+ end
221
+
222
+ module Exif
223
+ def read_header(srcfile, endian, offset)
224
+ return ExifHeader(srcfile, endian, offset)
225
+ end
226
+ end
227
+
228
+ module LittleEndian
229
+ def byte(str, signed=false)
230
+ if signed
231
+ return str[0,1].unpack('c')[0]
232
+ else
233
+ return str[0,1].unpack('C')[0]
234
+ end
235
+ end
236
+ def short(str, signed=false)
237
+ result = str[0,2].unpack('v')[0]
238
+ if signed
239
+ return ~ result unless result < 32768
240
+ end
241
+ return result
242
+ end
243
+ def int(str, signed=false)
244
+ result = str[0,4].unpack('V')[0]
245
+ if signed
246
+ return ~ result unless result < 2147483648
247
+ end
248
+ return result
249
+ end
250
+ end
251
+
252
+ module BigEndian
253
+ def byte(str, signed=false)
254
+ if signed
255
+ return str[0,1].unpack('c')[0]
256
+ else
257
+ return str[0,1].unpack('C')[0]
258
+ end
259
+ end
260
+ def short(str, signed=false)
261
+ result = str[0,2].unpack('n')[0]
262
+ if signed
263
+ return ~ result unless result < 32768
264
+ end
265
+ return result
266
+ end
267
+ def int(str, signed=false)
268
+ result = str[0,4].unpack('N')[0]
269
+ if signed
270
+ return ~ result unless result < 2147483648
271
+ end
272
+ return result
273
+ end
274
+ end
275
+
276
+ class Tiff < Base
277
+ def initialize(srcfile=nil)
278
+ super
279
+ header_bytes = @src.read(14)
280
+ case header_bytes[0...4]
281
+ when Cul::Image::Magic::TIFF_INTEL_LE
282
+ @endian = header_bytes[12]
283
+ when Cul::Image::Magic::TIFF_MOTOROLA_BE
284
+ @endian = header_bytes[12]
285
+ else
286
+ raise "Source file is not a tiff"
287
+ end
288
+ @src.rewind
289
+ tags = Cul::Image::Properties::Exif.process_file(srcfile)
290
+ if tags.include? 'Image ImageWidth'
291
+ self.width= tags['Image ImageWidth'].values[0]
292
+ end
293
+ if tags.include? 'Image ImageLength'
294
+ self.length= tags['Image ImageLength'].values[0]
295
+ end
296
+ if tags.include? 'Image XResolution'
297
+ self.x_sampling_freq= tags['Image XResolution'].values[0].inspect
298
+ end
299
+ if tags.include? 'Image YResolution'
300
+ self.y_sampling_freq= tags['Image YResolution'].values[0].inspect
301
+ end
302
+ if tags.include? 'Image ResolutionUnit'
303
+ if (tags['Image ResolutionUnit'].values[0] == 3)
304
+ self.sampling_unit='CentimeterSampling'
305
+ elsif (tags['Image ResolutionUnit'].values[0] == 2)
306
+ self.sampling_unit='InchSampling'
307
+ else
308
+ self.sampling_unit='NoAbsoluteSampling'
309
+ end
310
+ end
311
+ # do stuff with tags
312
+ self.extent= srcfile.stat.size unless srcfile.nil?
313
+ end
314
+ end
315
+
316
+ end
317
+ end
318
+ end
@@ -0,0 +1,7 @@
1
+ module Cul
2
+ module Image
3
+ module Properties
4
+ VERSION = "0.1.0"
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,51 @@
1
+ require 'cul_image_props/image/properties/version'
2
+ require 'cul_image_props/image/properties/types'
3
+ require 'cul_image_props/image/magic'
4
+ require 'open-uri'
5
+
6
+ module Cul
7
+ module Image
8
+ module Properties
9
+ def self.identify(src)
10
+ if src.is_a? String
11
+ src = open(src)
12
+ end
13
+ filesize = src.stat.size
14
+ result = nil
15
+ magic_bytes = ''
16
+ buf = ''
17
+ src.read(2,buf)
18
+ magic_bytes << buf
19
+ case magic_bytes
20
+ when Cul::Image::Magic::BMP
21
+ result = Cul::Image::Properties::Bmp.new(src)
22
+ when Cul::Image::Magic::JPEG
23
+ result = Cul::Image::Properties::Jpeg.new(src)
24
+ end
25
+
26
+ if result.nil?
27
+ src.read(2,buf)
28
+ magic_bytes << buf
29
+ case magic_bytes
30
+ when Cul::Image::Magic::TIFF_MOTOROLA_BE
31
+ result = Cul::Image::Properties::Tiff.new(src)
32
+ when Cul::Image::Magic::TIFF_INTEL_LE
33
+ result = Cul::Image::Properties::Tiff.new(src)
34
+ when Cul::Image::Magic::GIF
35
+ result = Cul::Image::Properties::Gif.new(src)
36
+ end
37
+ end
38
+ if result.nil?
39
+ src.read(4,buf)
40
+ magic_bytes << buf
41
+ if magic_bytes == Cul::Image::Magic::PNG
42
+ result = Cul::Image::Properties::Png.new(src)
43
+ else
44
+ puts magic_bytes.unpack('H2H2H2H2H2H2H2H2').inspect
45
+ end
46
+ end
47
+ return result
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,5 @@
1
+ require 'cul_image_props/image/properties'
2
+ module Cul
3
+ module Image
4
+ end
5
+ end
@@ -0,0 +1,11 @@
1
+ require 'cul_image_props/image/properties/version'
2
+ require 'cul_image_props/image'
3
+ module Cul
4
+ module Image
5
+ module Properties
6
+ def self.version
7
+ Cul::Image::Properties::VERSION
8
+ end
9
+ end
10
+ end
11
+ end
metadata ADDED
@@ -0,0 +1,118 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: cul_image_props
3
+ version: !ruby/object:Gem::Version
4
+ hash: 27
5
+ prerelease:
6
+ segments:
7
+ - 0
8
+ - 1
9
+ - 0
10
+ version: 0.1.0
11
+ platform: ruby
12
+ authors:
13
+ - Benjamin Armintor
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2012-02-14 00:00:00 Z
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
21
+ name: rspec
22
+ prerelease: false
23
+ requirement: &id001 !ruby/object:Gem::Requirement
24
+ none: false
25
+ requirements:
26
+ - - <
27
+ - !ruby/object:Gem::Version
28
+ hash: 15
29
+ segments:
30
+ - 2
31
+ - 0
32
+ - 0
33
+ version: 2.0.0
34
+ type: :development
35
+ version_requirements: *id001
36
+ - !ruby/object:Gem::Dependency
37
+ name: mocha
38
+ prerelease: false
39
+ requirement: &id002 !ruby/object:Gem::Requirement
40
+ none: false
41
+ requirements:
42
+ - - ">="
43
+ - !ruby/object:Gem::Version
44
+ hash: 43
45
+ segments:
46
+ - 0
47
+ - 9
48
+ - 8
49
+ version: 0.9.8
50
+ type: :development
51
+ version_requirements: *id002
52
+ - !ruby/object:Gem::Dependency
53
+ name: ruby-debug
54
+ prerelease: false
55
+ requirement: &id003 !ruby/object:Gem::Requirement
56
+ none: false
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ hash: 3
61
+ segments:
62
+ - 0
63
+ version: "0"
64
+ type: :development
65
+ version_requirements: *id003
66
+ description: Library for extracting basic image properties
67
+ email: armintor@gmail.com
68
+ executables: []
69
+
70
+ extensions: []
71
+
72
+ extra_rdoc_files: []
73
+
74
+ files:
75
+ - lib/cul_image_props/image/magic.rb
76
+ - lib/cul_image_props/image/properties/exif/constants.rb
77
+ - lib/cul_image_props/image/properties/exif/types.rb
78
+ - lib/cul_image_props/image/properties/exif.rb
79
+ - lib/cul_image_props/image/properties/types.rb
80
+ - lib/cul_image_props/image/properties/version.rb
81
+ - lib/cul_image_props/image/properties.rb
82
+ - lib/cul_image_props/image.rb
83
+ - lib/cul_image_props.rb
84
+ homepage:
85
+ licenses: []
86
+
87
+ post_install_message:
88
+ rdoc_options: []
89
+
90
+ require_paths:
91
+ - lib
92
+ required_ruby_version: !ruby/object:Gem::Requirement
93
+ none: false
94
+ requirements:
95
+ - - ">="
96
+ - !ruby/object:Gem::Version
97
+ hash: 3
98
+ segments:
99
+ - 0
100
+ version: "0"
101
+ required_rubygems_version: !ruby/object:Gem::Requirement
102
+ none: false
103
+ requirements:
104
+ - - ">="
105
+ - !ruby/object:Gem::Version
106
+ hash: 3
107
+ segments:
108
+ - 0
109
+ version: "0"
110
+ requirements: []
111
+
112
+ rubyforge_project:
113
+ rubygems_version: 1.8.13
114
+ signing_key:
115
+ specification_version: 3
116
+ summary: Library for extracting basic image properties
117
+ test_files: []
118
+