cul_image_props 0.1.0
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/lib/cul_image_props/image/magic.rb +22 -0
- data/lib/cul_image_props/image/properties/exif/constants.rb +1075 -0
- data/lib/cul_image_props/image/properties/exif/types.rb +542 -0
- data/lib/cul_image_props/image/properties/exif.rb +189 -0
- data/lib/cul_image_props/image/properties/types.rb +318 -0
- data/lib/cul_image_props/image/properties/version.rb +7 -0
- data/lib/cul_image_props/image/properties.rb +51 -0
- data/lib/cul_image_props/image.rb +5 -0
- data/lib/cul_image_props.rb +11 -0
- metadata +118 -0
@@ -0,0 +1,542 @@
|
|
1
|
+
module Cul
|
2
|
+
module Image
|
3
|
+
module Properties
|
4
|
+
module Exif
|
5
|
+
|
6
|
+
class FieldType
|
7
|
+
attr_accessor :length, :abbreviation, :name
|
8
|
+
def initialize(length, abb, name)
|
9
|
+
@length = length
|
10
|
+
@abbreviation = abb
|
11
|
+
@name = name
|
12
|
+
end
|
13
|
+
def [](index)
|
14
|
+
case index
|
15
|
+
when 0
|
16
|
+
return @length
|
17
|
+
when 1
|
18
|
+
return @abbreviation
|
19
|
+
when 2
|
20
|
+
return @name
|
21
|
+
else
|
22
|
+
raise format("Unexpected index %s", index.to_s)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
# first element of tuple is tag name, optional second element is
|
28
|
+
# another dictionary giving names to values
|
29
|
+
class TagName
|
30
|
+
attr_accessor :name, :value
|
31
|
+
def initialize(name, value=false)
|
32
|
+
@name = name
|
33
|
+
@value = value
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
class Ratio
|
38
|
+
# ratio object that eventually will be able to reduce itself to lowest
|
39
|
+
# common denominator for printing
|
40
|
+
attr_accessor :num, :den
|
41
|
+
def gcd(a, b)
|
42
|
+
if b == 1 or a == 1
|
43
|
+
return 1
|
44
|
+
elsif b == 0
|
45
|
+
return a
|
46
|
+
else
|
47
|
+
return gcd(b, a % b)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def initialize(num, den)
|
52
|
+
@num = num
|
53
|
+
@den = den
|
54
|
+
end
|
55
|
+
|
56
|
+
def inspect
|
57
|
+
self.reduce()
|
58
|
+
if @den == 1
|
59
|
+
return self.num.to_s
|
60
|
+
end
|
61
|
+
return format("%d/%d", @num, @den)
|
62
|
+
end
|
63
|
+
|
64
|
+
def reduce
|
65
|
+
div = gcd(@num, @den)
|
66
|
+
if div > 1
|
67
|
+
@num = @num / div
|
68
|
+
@den = @den / div
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
# for ease of dealing with tags
|
74
|
+
class IFD_Tag
|
75
|
+
attr_accessor :printable, :tag, :field_type, :values, :field_offset, :field_length
|
76
|
+
def initialize( printable, tag, field_type, values, field_offset, field_length)
|
77
|
+
# printable version of data
|
78
|
+
@printable = printable
|
79
|
+
# tag ID number
|
80
|
+
@tag = tag
|
81
|
+
# field type as index into FIELD_TYPES
|
82
|
+
@field_type = field_type
|
83
|
+
# offset of start of field in bytes from beginning of IFD
|
84
|
+
@field_offset = field_offset
|
85
|
+
# length of data field in bytes
|
86
|
+
@field_length = field_length
|
87
|
+
# either a string or array of data items
|
88
|
+
@values = values
|
89
|
+
end
|
90
|
+
|
91
|
+
def to_s
|
92
|
+
return @printable
|
93
|
+
end
|
94
|
+
|
95
|
+
def inspect
|
96
|
+
begin
|
97
|
+
s= format("(0x%04X) %s=%s @ %d", @tag,
|
98
|
+
FIELD_TYPES[@field_type][2],
|
99
|
+
@printable,
|
100
|
+
@field_offset)
|
101
|
+
rescue
|
102
|
+
s= format("(%s) %s=%s @ %s", @tag.to_s,
|
103
|
+
FIELD_TYPES[@field_type][2],
|
104
|
+
@printable,
|
105
|
+
@field_offset.to_s)
|
106
|
+
end
|
107
|
+
return s
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
# class that handles an EXIF header
|
112
|
+
class EXIF_header
|
113
|
+
attr_accessor :tags
|
114
|
+
def initialize(file, endian, offset, fake_exif, strict, detail=true)
|
115
|
+
@file = file
|
116
|
+
@endian = endian
|
117
|
+
@offset = offset
|
118
|
+
@fake_exif = fake_exif
|
119
|
+
@strict = strict
|
120
|
+
@detail = detail
|
121
|
+
@tags = {}
|
122
|
+
end
|
123
|
+
|
124
|
+
# extract multibyte integer in Motorola format (big/network endian)
|
125
|
+
def s2n_motorola(src)
|
126
|
+
x = 0
|
127
|
+
l = src.length
|
128
|
+
if l == 1
|
129
|
+
return src[0]
|
130
|
+
elsif l == 2
|
131
|
+
return src.unpack('n')[0]
|
132
|
+
elsif l == 4
|
133
|
+
return src.unpack('N')[0]
|
134
|
+
else
|
135
|
+
raise "Unexpected packed Fixnum length: " + src.length.to_s
|
136
|
+
end
|
137
|
+
end
|
138
|
+
# extract multibyte integer in Intel format (little endian)
|
139
|
+
def s2n_intel(src)
|
140
|
+
x = 0
|
141
|
+
y = 0
|
142
|
+
if l == 1
|
143
|
+
return src[0]
|
144
|
+
elsif l == 2
|
145
|
+
return src.unpack('v')[0]
|
146
|
+
elsif l == 4
|
147
|
+
return src.unpack('V')[0]
|
148
|
+
else
|
149
|
+
raise "Unexpected packed Fixnum length: " + src.length.to_s
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
def unpack_number(src, signed=false)
|
154
|
+
if @endian == 'I'
|
155
|
+
val=s2n_intel(src)
|
156
|
+
else
|
157
|
+
val=s2n_motorola(src)
|
158
|
+
end
|
159
|
+
# Sign extension ?
|
160
|
+
if signed
|
161
|
+
msb= 1 << (8*length-1)
|
162
|
+
if val & msb
|
163
|
+
val=val-(msb << 1)
|
164
|
+
end
|
165
|
+
end
|
166
|
+
return val
|
167
|
+
end
|
168
|
+
|
169
|
+
# convert slice to integer, based on sign and endian flags
|
170
|
+
# usually this offset is assumed to be relative to the beginning of the
|
171
|
+
# start of the EXIF information. For some cameras that use relative tags,
|
172
|
+
# this offset may be relative to some other starting point.
|
173
|
+
def s2n(offset, length, signed=false)
|
174
|
+
@file.seek(@offset+offset)
|
175
|
+
if @file.eof? and length != 0
|
176
|
+
# raise "Read past EOF"
|
177
|
+
puts "Read past EOF"
|
178
|
+
return 0
|
179
|
+
end
|
180
|
+
slice=@file.read(length)
|
181
|
+
if @endian == 'I'
|
182
|
+
val=s2n_intel(slice)
|
183
|
+
else
|
184
|
+
val=s2n_motorola(slice)
|
185
|
+
end
|
186
|
+
# Sign extension ?
|
187
|
+
if signed
|
188
|
+
msb= 1 << (8*length-1)
|
189
|
+
if val & msb
|
190
|
+
val=val-(msb << 1)
|
191
|
+
end
|
192
|
+
end
|
193
|
+
return val
|
194
|
+
end
|
195
|
+
|
196
|
+
# convert offset to string
|
197
|
+
def n2s(offset, length)
|
198
|
+
s = ''
|
199
|
+
length.times {
|
200
|
+
if @endian == 'I'
|
201
|
+
s = s + chr(offset & 0xFF)
|
202
|
+
else
|
203
|
+
s = chr(offset & 0xFF) + s
|
204
|
+
end
|
205
|
+
offset = offset >> 8
|
206
|
+
}
|
207
|
+
return s
|
208
|
+
end
|
209
|
+
|
210
|
+
# return first IFD
|
211
|
+
def first_IFD()
|
212
|
+
@file.seek(@offset + 4)
|
213
|
+
return unpack_number(@file.read(4))
|
214
|
+
end
|
215
|
+
|
216
|
+
# return pointer to next IFD
|
217
|
+
def next_IFD(ifd)
|
218
|
+
@file.seek(@offset + ifd)
|
219
|
+
entries = unpack_number(@file.read(2))
|
220
|
+
@file.seek((12*entries), IO::SEEK_CUR)
|
221
|
+
return unpack_number(@file.read(4))
|
222
|
+
end
|
223
|
+
|
224
|
+
# return list of IFDs in header
|
225
|
+
def list_IFDs()
|
226
|
+
i=self.first_IFD()
|
227
|
+
a=[]
|
228
|
+
while i != 0
|
229
|
+
a << i
|
230
|
+
i=self.next_IFD(i)
|
231
|
+
end
|
232
|
+
return a
|
233
|
+
end
|
234
|
+
|
235
|
+
# return list of entries in this IFD
|
236
|
+
# def dump_IFD(ifd, ifd_name, dict=EXIF_TAGS, relative=false, stop_tag='UNDEF')
|
237
|
+
def dump_IFD(ifd, ifd_name, opts)
|
238
|
+
opts = {:dict => EXIF_TAGS, :relative => false, :stop_tag => 'UNDEF'}.merge(opts)
|
239
|
+
dict = opts[:dict]
|
240
|
+
relative = opts[:relative]
|
241
|
+
stop_tag = opts[:stop_tag]
|
242
|
+
@file.seek(@offset + ifd)
|
243
|
+
entries = unpack_number(@file.read(2))
|
244
|
+
entries_ptr = ifd + 2
|
245
|
+
puts ifd_name + " had zero entries!" if entries == 0
|
246
|
+
(0 ... entries).each { |i|
|
247
|
+
# entry is index of start of this IFD in the file
|
248
|
+
entry = ifd + 2 + (12 * i)
|
249
|
+
@file.seek(@offset + entry)
|
250
|
+
tag_id = unpack_number(@file.read(2))
|
251
|
+
|
252
|
+
# get tag name early to avoid errors, help debug
|
253
|
+
tag_entry = dict[tag_id]
|
254
|
+
if tag_entry
|
255
|
+
tag_name = tag_entry.name
|
256
|
+
else
|
257
|
+
tag_name = 'Tag 0x%04X' % tag_id
|
258
|
+
end
|
259
|
+
|
260
|
+
# ignore certain tags for faster processing
|
261
|
+
if not (not @detail and IGNORE_TAGS.include? tag_id)
|
262
|
+
# The 12 byte Tag format is ID (short) TYPE (short) COUNT (long) VALUE (long)
|
263
|
+
# if actual values would exceed 4 bytes (long), VALUE
|
264
|
+
# is instead a pointer to the actual values.
|
265
|
+
field_type = unpack_number(@file.read(2))
|
266
|
+
|
267
|
+
# unknown field type
|
268
|
+
if 0 > field_type or field_type >= FIELD_TYPES.length
|
269
|
+
if not self.strict
|
270
|
+
next
|
271
|
+
else
|
272
|
+
raise format("unknown type %d in tag 0x%04X", field_type, tag)
|
273
|
+
end
|
274
|
+
end
|
275
|
+
typelen = FIELD_TYPES[field_type][0]
|
276
|
+
count = unpack_number(@file.read(4))
|
277
|
+
|
278
|
+
# If the value exceeds 4 bytes, it is a pointer to values.
|
279
|
+
if (count * typelen) > 4
|
280
|
+
# Note that 'relative' is a fix for the Nikon type 3 makernote.
|
281
|
+
field_offset = unpack_number(@file.read(4))
|
282
|
+
if relative
|
283
|
+
field_offset = field_offset + ifd - 8
|
284
|
+
if @fake_exif
|
285
|
+
field_offset = field_offset + 18
|
286
|
+
end
|
287
|
+
end
|
288
|
+
else
|
289
|
+
field_offset = entry + 8
|
290
|
+
end
|
291
|
+
|
292
|
+
if field_type == 2
|
293
|
+
# special case => null-terminated ASCII string
|
294
|
+
# XXX investigate
|
295
|
+
# sometimes gets too big to fit in int value
|
296
|
+
if count != 0 and count < (2**31)
|
297
|
+
@file.seek(@offset + field_offset)
|
298
|
+
values = @file.read(count)
|
299
|
+
#print values
|
300
|
+
# Drop any garbage after a null.
|
301
|
+
values = values.split('\x00', 1)[0]
|
302
|
+
else
|
303
|
+
values = ''
|
304
|
+
end
|
305
|
+
else
|
306
|
+
values = []
|
307
|
+
signed = [6, 8, 9, 10].include? field_type
|
308
|
+
|
309
|
+
# @todo investigate
|
310
|
+
# some entries get too big to handle could be malformed file
|
311
|
+
if count < 1000 or tag_name == 'MakerNote'
|
312
|
+
@file.seek(@offset + field_offset)
|
313
|
+
count.times {
|
314
|
+
if field_type == 5 or field_type == 10
|
315
|
+
# a ratio
|
316
|
+
value = Ratio.new(unpack_number(@file.read(4), signed),
|
317
|
+
unpack_number(@file.read(4), signed) )
|
318
|
+
else
|
319
|
+
value = unpack_number(@file.read(typelen), signed)
|
320
|
+
end
|
321
|
+
values << value
|
322
|
+
}
|
323
|
+
end
|
324
|
+
end
|
325
|
+
# now 'values' is either a string or an array
|
326
|
+
if count == 1 and field_type != 2
|
327
|
+
printable=values[0].to_s
|
328
|
+
elsif count > 50 and values.length > 20
|
329
|
+
printable=str( values[0 =>20] )[0 =>-1] + ", ... ]"
|
330
|
+
else
|
331
|
+
printable=values.inspect
|
332
|
+
end
|
333
|
+
# compute printable version of values
|
334
|
+
if tag_entry
|
335
|
+
if tag_entry.value
|
336
|
+
# optional 2nd tag element is present
|
337
|
+
if tag_entry.value.respond_to? :call
|
338
|
+
# call mapping function
|
339
|
+
printable = tag_entry.value.call(values)
|
340
|
+
else
|
341
|
+
printable = ''
|
342
|
+
values.each { |i|
|
343
|
+
# use lookup table for this tag
|
344
|
+
printable += (tag_entry.value.include? i)?tag_entry.value[i] : i.inspect
|
345
|
+
}
|
346
|
+
end
|
347
|
+
end
|
348
|
+
end
|
349
|
+
self.tags[ifd_name + ' ' + tag_name] = IFD_Tag.new(printable, tag_id,
|
350
|
+
field_type,
|
351
|
+
values, field_offset,
|
352
|
+
count * typelen)
|
353
|
+
end
|
354
|
+
if tag_name == stop_tag
|
355
|
+
break
|
356
|
+
end
|
357
|
+
}
|
358
|
+
end
|
359
|
+
|
360
|
+
# extract uncompressed TIFF thumbnail (like pulling teeth)
|
361
|
+
# we take advantage of the pre-existing layout in the thumbnail IFD as
|
362
|
+
# much as possible
|
363
|
+
def extract_TIFF_thumbnail(thumb_ifd)
|
364
|
+
entries = self.s2n(thumb_ifd, 2)
|
365
|
+
# this is header plus offset to IFD ...
|
366
|
+
if @endian == 'M'
|
367
|
+
tiff = 'MM\x00*\x00\x00\x00\x08'
|
368
|
+
else
|
369
|
+
tiff = 'II*\x00\x08\x00\x00\x00'
|
370
|
+
end
|
371
|
+
# ... plus thumbnail IFD data plus a null "next IFD" pointer
|
372
|
+
self.file.seek(self.offset+thumb_ifd)
|
373
|
+
tiff += self.file.read(entries*12+2)+'\x00\x00\x00\x00'
|
374
|
+
|
375
|
+
# fix up large value offset pointers into data area
|
376
|
+
(0...entries).each { |i|
|
377
|
+
entry = thumb_ifd + 2 + 12 * i
|
378
|
+
tag = self.s2n(entry, 2)
|
379
|
+
field_type = self.s2n(entry+2, 2)
|
380
|
+
typelen = FIELD_TYPES[field_type][0]
|
381
|
+
count = self.s2n(entry+4, 4)
|
382
|
+
oldoff = self.s2n(entry+8, 4)
|
383
|
+
# start of the 4-byte pointer area in entry
|
384
|
+
ptr = i * 12 + 18
|
385
|
+
# remember strip offsets location
|
386
|
+
if tag == 0x0111
|
387
|
+
strip_off = ptr
|
388
|
+
strip_len = count * typelen
|
389
|
+
end
|
390
|
+
# is it in the data area?
|
391
|
+
if count * typelen > 4
|
392
|
+
# update offset pointer (nasty "strings are immutable" crap)
|
393
|
+
# should be able to say "tiff[ptr..ptr+4]=newoff"
|
394
|
+
newoff = len(tiff)
|
395
|
+
tiff = tiff[ 0..ptr] + self.n2s(newoff, 4) + tiff[ptr+4...tiff.length]
|
396
|
+
# remember strip offsets location
|
397
|
+
if tag == 0x0111
|
398
|
+
strip_off = newoff
|
399
|
+
strip_len = 4
|
400
|
+
end
|
401
|
+
# get original data and store it
|
402
|
+
self.file.seek(self.offset + oldoff)
|
403
|
+
tiff += self.file.read(count * typelen)
|
404
|
+
end
|
405
|
+
}
|
406
|
+
# add pixel strips and update strip offset info
|
407
|
+
old_offsets = self.tags['Thumbnail StripOffsets'].values
|
408
|
+
old_counts = self.tags['Thumbnail StripByteCounts'].values
|
409
|
+
(0...len(old_offsets)).each { |i|
|
410
|
+
# update offset pointer (more nasty "strings are immutable" crap)
|
411
|
+
offset = self.n2s(len(tiff), strip_len)
|
412
|
+
tiff = tiff[ 0..strip_off] + offset + tiff[strip_off + strip_len ... tiff.length]
|
413
|
+
strip_off += strip_len
|
414
|
+
# add pixel strip to end
|
415
|
+
self.file.seek(self.offset + old_offsets[i])
|
416
|
+
tiff += self.file.read(old_counts[i])
|
417
|
+
}
|
418
|
+
self.tags['TIFFThumbnail'] = tiff
|
419
|
+
end
|
420
|
+
|
421
|
+
# decode all the camera-specific MakerNote formats
|
422
|
+
|
423
|
+
# Note is the data that comprises this MakerNote. The MakerNote will
|
424
|
+
# likely have pointers in it that point to other parts of the file. We'll
|
425
|
+
# use self.offset as the starting point for most of those pointers, since
|
426
|
+
# they are relative to the beginning of the file.
|
427
|
+
#
|
428
|
+
# If the MakerNote is in a newer format, it may use relative addressing
|
429
|
+
# within the MakerNote. In that case we'll use relative addresses for the
|
430
|
+
# pointers.
|
431
|
+
#
|
432
|
+
# As an aside => it's not just to be annoying that the manufacturers use
|
433
|
+
# relative offsets. It's so that if the makernote has to be moved by the
|
434
|
+
# picture software all of the offsets don't have to be adjusted. Overall,
|
435
|
+
# this is probably the right strategy for makernotes, though the spec is
|
436
|
+
# ambiguous. (The spec does not appear to imagine that makernotes would
|
437
|
+
# follow EXIF format internally. Once they did, it's ambiguous whether
|
438
|
+
# the offsets should be from the header at the start of all the EXIF info,
|
439
|
+
# or from the header at the start of the makernote.)
|
440
|
+
def decode_maker_note()
|
441
|
+
note = self.tags['EXIF MakerNote']
|
442
|
+
|
443
|
+
# Some apps use MakerNote tags but do not use a format for which we
|
444
|
+
# have a description, so just do a raw dump for these.
|
445
|
+
|
446
|
+
make = self.tags['Image Make'].printable
|
447
|
+
|
448
|
+
# Nikon
|
449
|
+
# The maker note usually starts with the word Nikon, followed by the
|
450
|
+
# type of the makernote (1 or 2, as a short). If the word Nikon is
|
451
|
+
# not at the start of the makernote, it's probably type 2, since some
|
452
|
+
# cameras work that way.
|
453
|
+
if make.include? 'NIKON'
|
454
|
+
if note.values[0,7] == [78, 105, 107, 111, 110, 0, 1]
|
455
|
+
self.dump_IFD(note.field_offset+8, 'MakerNote',
|
456
|
+
:dict=>MAKERNOTE_NIKON_OLDER_TAGS)
|
457
|
+
elsif note.values[0, 7] == [78, 105, 107, 111, 110, 0, 2]
|
458
|
+
if note.values[12,2] != [0, 42] and note.values[12,2] != [42, 0]
|
459
|
+
raise "Missing marker tag '42' in MakerNote."
|
460
|
+
end
|
461
|
+
# skip the Makernote label and the TIFF header
|
462
|
+
self.dump_IFD(note.field_offset+10+8, 'MakerNote',
|
463
|
+
:dict=>MAKERNOTE_NIKON_NEWER_TAGS, :relative=>true)
|
464
|
+
else
|
465
|
+
# E99x or D1
|
466
|
+
self.dump_IFD(note.field_offset, 'MakerNote',
|
467
|
+
:dict=>MAKERNOTE_NIKON_NEWER_TAGS)
|
468
|
+
end
|
469
|
+
return
|
470
|
+
end
|
471
|
+
# Olympus
|
472
|
+
if make.index('OLYMPUS') == 0
|
473
|
+
self.dump_IFD(note.field_offset+8, 'MakerNote',
|
474
|
+
:dict=>MAKERNOTE_OLYMPUS_TAGS)
|
475
|
+
return
|
476
|
+
end
|
477
|
+
# Casio
|
478
|
+
if make.include? 'CASIO' or make.include? 'Casio'
|
479
|
+
self.dump_IFD(note.field_offset, 'MakerNote',
|
480
|
+
:dict=>MAKERNOTE_CASIO_TAGS)
|
481
|
+
return
|
482
|
+
end
|
483
|
+
# Fujifilm
|
484
|
+
if make == 'FUJIFILM'
|
485
|
+
# bug => everything else is "Motorola" endian, but the MakerNote
|
486
|
+
# is "Intel" endian
|
487
|
+
endian = self.endian
|
488
|
+
self.endian = 'I'
|
489
|
+
# bug => IFD offsets are from beginning of MakerNote, not
|
490
|
+
# beginning of file header
|
491
|
+
offset = self.offset
|
492
|
+
self.offset += note.field_offset
|
493
|
+
# process note with bogus values (note is actually at offset 12)
|
494
|
+
self.dump_IFD(12, 'MakerNote', :dict=>MAKERNOTE_FUJIFILM_TAGS)
|
495
|
+
# reset to correct values
|
496
|
+
self.endian = endian
|
497
|
+
self.offset = offset
|
498
|
+
return
|
499
|
+
end
|
500
|
+
# Canon
|
501
|
+
if make == 'Canon'
|
502
|
+
self.dump_IFD(note.field_offset, 'MakerNote',
|
503
|
+
:dict=>MAKERNOTE_CANON_TAGS)
|
504
|
+
[['MakerNote Tag 0x0001', MAKERNOTE_CANON_TAG_0x001],
|
505
|
+
['MakerNote Tag 0x0004', MAKERNOTE_CANON_TAG_0x004]].each { |i|
|
506
|
+
begin
|
507
|
+
self.canon_decode_tag(self.tags[i[0]].values, i[1]) # gd added
|
508
|
+
rescue
|
509
|
+
end
|
510
|
+
}
|
511
|
+
return
|
512
|
+
end
|
513
|
+
end
|
514
|
+
|
515
|
+
# XXX TODO decode Olympus MakerNote tag based on offset within tag
|
516
|
+
def olympus_decode_tag(value, dict)
|
517
|
+
end
|
518
|
+
|
519
|
+
# decode Canon MakerNote tag based on offset within tag
|
520
|
+
# see http =>//www.burren.cx/david/canon.html by David Burren
|
521
|
+
def canon_decode_tag(value, dict)
|
522
|
+
(1 ... len(value)).each { |i|
|
523
|
+
x=dict.get(i, ['Unknown'])
|
524
|
+
|
525
|
+
name=x[0]
|
526
|
+
if len(x) > 1
|
527
|
+
val=x[1].get(value[i], 'Unknown')
|
528
|
+
else
|
529
|
+
val=value[i]
|
530
|
+
end
|
531
|
+
# it's not a real IFD Tag but we fake one to make everybody
|
532
|
+
# happy. this will have a "proprietary" type
|
533
|
+
self.tags['MakerNote '+name]=IFD_Tag(str(val), None, 0, None,
|
534
|
+
None, None)
|
535
|
+
}
|
536
|
+
end
|
537
|
+
end # EXIF_header
|
538
|
+
|
539
|
+
end # ::Exif
|
540
|
+
end # ::Properties
|
541
|
+
end # ::Image
|
542
|
+
end # ::Cul
|