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,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