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.
@@ -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