aviglitch 0.1.6 → 0.2.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,557 @@
1
+ module AviGlitch
2
+
3
+ # Avi parses the passed RIFF-AVI file and maintains binary data as
4
+ # a structured object.
5
+ # It contains headers, frame's raw data, and indices of frames.
6
+ # The AviGlitch library accesses the data through this class internally.
7
+ #
8
+ class Avi
9
+
10
+ # :stopdoc:
11
+
12
+ # RiffChunk represents a parsed RIFF chunk.
13
+ class RiffChunk
14
+
15
+ attr_accessor :id, :list, :value, :binsize
16
+
17
+ def initialize id, size, value, list = false
18
+ @binsize = size
19
+ @is_list = list.kind_of? Array
20
+ unless is_list?
21
+ @id = id
22
+ @value = value
23
+ else
24
+ @id = value
25
+ @list = id
26
+ @value = list
27
+ end
28
+ end
29
+
30
+ def is_list?
31
+ @is_list
32
+ end
33
+
34
+ def children id
35
+ if is_list?
36
+ value.filter do |chk|
37
+ chk.id == id
38
+ end
39
+ else
40
+ []
41
+ end
42
+ end
43
+
44
+ def child id
45
+ children(id).first
46
+ end
47
+
48
+ def search *args
49
+ a1 = args.shift
50
+ r = value.filter { |v|
51
+ v.id == a1
52
+ }.collect { |v|
53
+ if args.size > 0
54
+ v.search *args
55
+ else
56
+ v
57
+ end
58
+ }
59
+ r.flatten
60
+ end
61
+
62
+ def inspect
63
+ if @is_list
64
+ "{list: \"#{list}\", id: \"#{id}\", binsize: #{binsize}, value: #{value}}"
65
+ elsif !value.nil?
66
+ "{id: \"#{id}\", binsize: #{binsize}, value: \"#{value}\"}"
67
+ else
68
+ "{id: \"#{id}\", binsize: #{binsize}}"
69
+ end
70
+ end
71
+
72
+ end
73
+
74
+ # :startdoc:
75
+
76
+ MAX_RIFF_SIZE = 1024 ** 3
77
+ # List of indices for 'movi' data.
78
+ attr_accessor :indices
79
+ # Object which represents RIFF structure.
80
+ attr_accessor :riff
81
+
82
+ attr_accessor :path, :movi, :tmpdir #:nodoc:
83
+ protected :path, :path=, :movi, :movi=
84
+
85
+ ##
86
+ # Generates an instance.
87
+ def initialize path = nil
88
+ return unless @movi.nil? # don't reconfigure the path when cloning
89
+ self.path = path unless path.nil?
90
+ end
91
+
92
+ ##
93
+ # Set +path+ of the source file.
94
+ def path= path #:nodoc:
95
+ @path = path
96
+ File.open(path, 'rb') do |io|
97
+ @movi = Tempfile.new 'aviglitch', @tmpdir, binmode: true
98
+ @riff = []
99
+ @indices = []
100
+ @superidx = []
101
+ @was_avi2 = false
102
+ io.rewind
103
+ parse_riff io, @riff
104
+ if was_avi2?
105
+ @indices.sort_by! { |ix| ix[:offset] }
106
+ end
107
+ end
108
+ end
109
+
110
+ ##
111
+ # Parses the passed RIFF formated file recursively.
112
+ def parse_riff io, target, len = 0, is_movi = false
113
+ offset = io.pos
114
+ binoffset = @movi.pos
115
+ while id = io.read(4) do
116
+ if len > 0 && io.pos >= offset + len
117
+ io.pos -= 4
118
+ break
119
+ end
120
+ size = io.read(4).unpack('V').first
121
+ if id == 'RIFF' || id == 'LIST'
122
+ lid = io.read(4)
123
+ newarr = []
124
+ chunk = RiffChunk.new id, size, lid, newarr
125
+ target << chunk
126
+ parse_riff io, newarr, size, lid == 'movi'
127
+ else
128
+ value = nil
129
+ if is_movi
130
+ if id =~ /^ix/
131
+ v = io.read size
132
+ # confirm the super index surely has information
133
+ @superidx.each do |sidx|
134
+ nent = sidx[4, 4].unpack('v').first
135
+ cid = sidx[8, 4]
136
+ nent.times do |i|
137
+ ent = sidx[24 + 16 * i, 16]
138
+ # we can check other informations thuogh
139
+ valid = ent[0, 8].unpack('q').first == io.pos - v.size - 8
140
+ parse_avi2_indices(v, binoffset) if valid
141
+ end
142
+ end
143
+ else
144
+ io.pos -= 8
145
+ v = io.read(size + 8)
146
+ @movi.print v
147
+ @movi.print "\0" if size % 2 == 1
148
+ end
149
+ elsif id == 'idx1'
150
+ v = io.read size
151
+ parse_avi1_indices v unless was_avi2?
152
+ else
153
+ value = io.read size
154
+ if id == 'indx'
155
+ @superidx << value
156
+ @was_avi2 = true
157
+ end
158
+ end
159
+ chunk = RiffChunk.new id, size, value
160
+ target << chunk
161
+ io.pos += 1 if size % 2 == 1
162
+ end
163
+ end
164
+ end
165
+
166
+ ##
167
+ # Closes the file.
168
+ def close
169
+ @movi.close!
170
+ end
171
+
172
+ ##
173
+ # Detects the passed file was an AVI2.0 file.
174
+ def was_avi2?
175
+ @was_avi2
176
+ end
177
+
178
+ ##
179
+ # Detects the current data will be an AVI2.0 file.
180
+ def is_avi2?
181
+ @movi.size >= MAX_RIFF_SIZE
182
+ end
183
+
184
+ ##
185
+ # Saves data to AVI formatted file.
186
+ def output path
187
+ @index_pos = 0
188
+ # prepare headers by reusing existing ones
189
+ strl = search 'hdrl', 'strl'
190
+ if is_avi2?
191
+ # indx
192
+ vid_frames_size = 0
193
+ @indexinfo = @indices.collect { |ix|
194
+ vid_frames_size += 1 if ix[:id] =~ /d[bc]$/
195
+ ix[:id]
196
+ }.uniq.sort.collect { |d|
197
+ [d, {}]
198
+ }.to_h # should be like: {"00dc"=>{}, "01wb"=>{}}
199
+ strl.each_with_index do |sl, i|
200
+ indx = sl.child 'indx'
201
+ if indx.nil?
202
+ indx = RiffChunk.new('indx', 4120, "\0" * 4120)
203
+ indx.value[0, 8] = [4, 0, 0, 0].pack('vccV')
204
+ sl.value.push indx
205
+ else
206
+ indx.value[4, 4] = [0].pack('V')
207
+ indx.value[24..-1] = "\0" * (indx.value.size - 24)
208
+ end
209
+ preid = indx.value[8, 4]
210
+ info = @indexinfo.find do |key, val|
211
+ # more strict way must exist though..
212
+ if preid == "\0\0\0\0"
213
+ key.start_with? "%02d" % i
214
+ else
215
+ key == preid
216
+ end
217
+ end
218
+ indx.value[8, 4] = info.first if preid == "\0\0\0\0"
219
+ info.last[:indx] = indx
220
+ info.last[:fcc] = 'ix' + info.first[0, 2]
221
+ info.last[:cur] = []
222
+ end
223
+ # odml
224
+ odml = search('hdrl', 'odml').first
225
+ if odml.nil?
226
+ odml = RiffChunk.new(
227
+ 'LIST', 260, 'odml', [RiffChunk.new('dmlh', 248, "\0" * 248)]
228
+ )
229
+ @riff.first.child('hdrl').value.push odml
230
+ end
231
+ odml.child('dmlh').value[0, 4] = [@indices.size].pack('V')
232
+ else
233
+ strl.each do |sl|
234
+ indx = sl.child 'indx'
235
+ unless indx.nil?
236
+ sl.value.delete indx
237
+ end
238
+ end
239
+ end
240
+
241
+ # movi
242
+ write_movi = ->(io) do
243
+ vid_frames_size = 0
244
+ io.print 'LIST'
245
+ io.print "\0\0\0\0"
246
+ data_offset = io.pos
247
+ io.print 'movi'
248
+ while io.pos - data_offset <= MAX_RIFF_SIZE
249
+ ix = @indices[@index_pos]
250
+ @indexinfo[ix[:id]][:cur] << {
251
+ pos: io.pos, size: ix[:size], flag: ix[:flag]
252
+ } if is_avi2?
253
+ io.print ix[:id]
254
+ vid_frames_size += 1 if ix[:id] =~ /d[bc]$/
255
+ io.print [ix[:size]].pack('V')
256
+ @movi.pos += 8
257
+ io.print @movi.read(ix[:size])
258
+ if ix[:size] % 2 == 1
259
+ io.print "\0"
260
+ @movi.pos += 1
261
+ end
262
+ @index_pos += 1
263
+ break if @index_pos > @indices.size - 1
264
+ end
265
+ # standard index
266
+ if is_avi2?
267
+ @indexinfo.each do |key, info|
268
+ ix_offset = io.pos
269
+ io.print info[:fcc]
270
+ io.print [24 + 8 * info[:cur].size].pack('V')
271
+ io.print [2, 0, 1, info[:cur].size].pack('vccV')
272
+ io.print key
273
+ io.print [data_offset, 0].pack('qV')
274
+ info[:cur].each.with_index do |cur, i|
275
+ io.print [cur[:pos] - data_offset + 8].pack('V') # 8 for LIST####
276
+ sz = cur[:size]
277
+ if cur[:flag] & Frame::AVIIF_KEYFRAME == 0 # is not keyframe
278
+ sz = sz | 0b1000_0000_0000_0000_0000_0000_0000_0000
279
+ end
280
+ io.print [sz].pack('V')
281
+ end
282
+ # rewrite indx
283
+ indx = info[:indx]
284
+ nent = indx.value[4, 4].unpack('V').first + 1
285
+ indx.value[4, 4] = [nent].pack('V')
286
+ indx.value[24 + 16 * (nent - 1), 16] = [
287
+ ix_offset, io.pos - ix_offset, info[:cur].size
288
+ ].pack('qVV')
289
+ io.pos = expected_position_of(indx) + 8
290
+ io.print indx.value
291
+ # clean up
292
+ info[:cur] = []
293
+ io.seek 0, IO::SEEK_END
294
+ end
295
+ end
296
+ # size of movi
297
+ size = io.pos - data_offset
298
+ io.pos = data_offset - 4
299
+ io.print [size].pack('V')
300
+ io.seek 0, IO::SEEK_END
301
+ io.print "\0" if size % 2 == 1
302
+ vid_frames_size
303
+ end
304
+
305
+ File.open(path, 'w+') do |io|
306
+ io.binmode
307
+ @movi.rewind
308
+ # normal AVI
309
+ # header
310
+ io.print 'RIFF'
311
+ io.print "\0\0\0\0"
312
+ io.print 'AVI '
313
+ @riff.first.value.each do |chunk|
314
+ break if chunk.id == 'movi'
315
+ print_chunk io, chunk
316
+ end
317
+ # movi
318
+ vid_size = write_movi.call io
319
+ # rewrite frame count in avih header
320
+ io.pos = 48
321
+ io.print [vid_size].pack('V')
322
+ io.seek 0, IO::SEEK_END
323
+ # idx1
324
+ io.print 'idx1'
325
+ io.print [@index_pos * 16].pack('V')
326
+ @indices[0..(@index_pos - 1)].each do |ix|
327
+ io.print ix[:id] + [ix[:flag], ix[:offset] + 4, ix[:size]].pack('V3')
328
+ end
329
+ # rewrite riff chunk size
330
+ avisize = io.pos - 8
331
+ io.pos = 4
332
+ io.print [avisize].pack('V')
333
+ io.seek 0, IO::SEEK_END
334
+
335
+ # AVI2.0
336
+ while @index_pos < @indices.size
337
+ io.print 'RIFF'
338
+ io.print "\0\0\0\0"
339
+ riff_offset = io.pos
340
+ io.print 'AVIX'
341
+ # movi
342
+ write_movi.call io
343
+ # rewrite total chunk size
344
+ avisize = io.pos - riff_offset
345
+ io.pos = riff_offset - 4
346
+ io.print [avisize].pack('V')
347
+ io.seek 0, IO::SEEK_END
348
+ end
349
+ end
350
+ end
351
+
352
+ ##
353
+ # Provides internal accesses to movi binary data.
354
+ # It requires the yield block to return an array of pair values
355
+ # which consists of new indices array and new movi binary data.
356
+ def process_movi &block
357
+ @movi.rewind
358
+ newindices, newmovi = block.call @indices, @movi
359
+ unless @indices == newindices
360
+ @indices.replace newindices
361
+ end
362
+ unless @movi == newmovi
363
+ @movi.rewind
364
+ newmovi.rewind
365
+ while d = newmovi.read(BUFFER_SIZE) do
366
+ @movi.print d
367
+ end
368
+ eof = @movi.pos
369
+ @movi.truncate eof
370
+ end
371
+ end
372
+
373
+ ##
374
+ # Searches and returns RIFF values with the passed search +args+.
375
+ # +args+ should point the ids of the tree structured RIFF data
376
+ # under the 'AVI ' chunk without omission, like:
377
+ #
378
+ # avi.search 'hdrl', 'strl', 'indx'
379
+ #
380
+ # It returns a list of RiffChunk object which can be modified directly.
381
+ # (RiffChunk class which is returned through this method also has a #search
382
+ # method with the same interface as this class.)
383
+ # This method only seeks in the first RIFF 'AVI ' tree.
384
+ def search *args
385
+ @riff.first.search *args
386
+ end
387
+
388
+ ##
389
+ # Returns true if +other+'s indices are same as self's indices.
390
+ def == other
391
+ self.indices == other.indices
392
+ end
393
+
394
+ def inspect #:nodoc:
395
+ "#<#{self.class.name}:#{sprintf("0x%x", object_id)} @movi=#{@movi.inspect}>"
396
+ end
397
+
398
+ def initialize_copy avi #:nodoc:
399
+ md = Marshal.dump avi.indices
400
+ @indices = Marshal.load md
401
+ md = Marshal.dump avi.riff
402
+ @riff = Marshal.load md
403
+ newmovi = Tempfile.new 'aviglitch-clone', @tmpdir, binmode: true
404
+ movipos = avi.movi.pos
405
+ avi.movi.rewind
406
+ while d = avi.movi.read(BUFFER_SIZE) do
407
+ newmovi.print d
408
+ end
409
+ avi.movi.pos = movipos
410
+ newmovi.rewind
411
+ @movi = newmovi
412
+ end
413
+
414
+ def print_chunk io, chunk #:nodoc:
415
+ offset = io.pos
416
+ if chunk.is_list?
417
+ io.print chunk.list
418
+ io.print "\0\0\0\0"
419
+ io.print chunk.id
420
+ chunk.value.each do |c|
421
+ print_chunk io, c
422
+ end
423
+ else
424
+ io.print chunk.id
425
+ io.print "\0\0\0\0"
426
+ io.print chunk.value
427
+ end
428
+ # rewrite size
429
+ size = io.pos - offset - 8
430
+ io.pos = offset + 4
431
+ io.print [size].pack('V')
432
+ io.seek 0, IO::SEEK_END
433
+ io.print "\0" if size % 2 == 1
434
+ end
435
+
436
+ def expected_position_of chunk #:nodoc:
437
+ pos = -1
438
+ cur = 12
439
+ seek = -> (chk) do
440
+ if chk === chunk
441
+ pos = cur
442
+ return
443
+ end
444
+ if chk.is_list?
445
+ cur += 12
446
+ chk.value.each do |c|
447
+ seek.call c
448
+ end
449
+ else
450
+ cur += 8
451
+ cur += chk.value.nil? ? chk.binsize : chk.value.size
452
+ end
453
+ end
454
+ headers = @riff.first.value
455
+ headers.each do |c|
456
+ seek.call c
457
+ end
458
+ pos
459
+ end
460
+
461
+ def parse_avi1_indices data #:nodoc:
462
+ # The function Frames#fix_offsets_if_needed in prev versions was now removed.
463
+ i = 0
464
+ while i * 16 < data.size do
465
+ @indices << {
466
+ :id => data[i * 16, 4],
467
+ :flag => data[i * 16 + 4, 4].unpack('V').first,
468
+ :offset => data[i * 16 + 8, 4].unpack('V').first - 4,
469
+ :size => data[i * 16 + 12, 4].unpack('V').first,
470
+ }
471
+ i += 1
472
+ end
473
+ end
474
+
475
+ def parse_avi2_indices data, offset #:nodoc:
476
+ id = data[8, 4]
477
+ nent = data[4, 4].unpack('V').first
478
+ h = 24
479
+ i = 0
480
+ while h + i * 8 < data.size
481
+ moffset = data[h + i * 8, 4].unpack('V').first
482
+ msize = data[h + i * 8 + 4, 4].unpack('V').first
483
+ of = offset + moffset - 12 # 12 for movi + 00dc####
484
+ # bit 31 is set if this is NOT a keyframe
485
+ fl = (msize >> 31 == 1) ? 0 : Frame::AVIIF_KEYFRAME
486
+ sz = msize & 0b0111_1111_1111_1111_1111_1111_1111_1111
487
+ @indices << {
488
+ :id => id,
489
+ :flag => fl,
490
+ :offset => of,
491
+ :size => sz,
492
+ }
493
+ i += 1
494
+ end
495
+ end
496
+
497
+ private :print_chunk, :expected_position_of,
498
+ :parse_avi1_indices, :parse_avi2_indices
499
+
500
+ class << self
501
+
502
+ ##
503
+ # Parses the +file+ and returns the RIFF structure.
504
+ def rifftree file, out = nil
505
+ returnable = out.nil?
506
+ out = StringIO.new if returnable
507
+
508
+ parse = ->(io, depth = 0, len = 0) do
509
+ offset = io.pos
510
+ while id = io.read(4) do
511
+ if len > 0 && io.pos >= offset + len
512
+ io.pos -= 4
513
+ break
514
+ end
515
+ size = io.read(4).unpack('V').first
516
+ str = depth > 0 ? ' ' * depth + id : id
517
+ if id =~ /^(?:RIFF|LIST)$/
518
+ lid = io.read(4)
519
+ str << (' (%d)' % size) + " ’#{lid}’\n"
520
+ out.print str
521
+ parse.call io, depth + 1, size
522
+ else
523
+ str << (' (%d)' % size ) + "\n"
524
+ out.print str
525
+ io.pos += size
526
+ io.pos += 1 if size % 2 == 1
527
+ end
528
+ end
529
+ end
530
+
531
+ io = file
532
+ is_io = file.respond_to?(:seek) # Probably IO.
533
+ io = File.open(file, 'rb') unless is_io
534
+ begin
535
+ io.rewind
536
+ parse.call io
537
+ io.rewind
538
+ ensure
539
+ io.close unless is_io
540
+ end
541
+
542
+ if returnable
543
+ out.rewind
544
+ out.read
545
+ end
546
+ end
547
+
548
+ ##
549
+ # Parses the +file+ and prints the RIFF structure to stdout.
550
+ def print_rifftree file
551
+ Avi.rifftree file, $stdout
552
+ end
553
+
554
+ end
555
+
556
+ end
557
+ end
@@ -7,33 +7,31 @@ module AviGlitch
7
7
 
8
8
  # AviGlitch::Frames object generated from the +file+.
9
9
  attr_reader :frames
10
- # The input file (copied tempfile).
11
- attr_reader :file
10
+ # The input file
11
+ attr_reader :avi
12
12
 
13
13
  ##
14
14
  # Creates a new instance of AviGlitch::Base, open the file and
15
15
  # make it ready to manipulate.
16
- # It requires +path+ as Pathname.
17
- def initialize path
18
- File.open(path, 'rb') do |f|
19
- # copy as tempfile
20
- @file = Tempfile.new 'aviglitch', binmode: true
21
- f.rewind
22
- while d = f.read(BUFFER_SIZE) do
23
- @file.print d
16
+ # It requires +path+ as Pathname or an instance of AviGlirtch::Avi.
17
+ def initialize path_or_object, tmpdir: nil
18
+ if path_or_object.kind_of?(Avi)
19
+ @avi = path_or_object
20
+ else
21
+ unless AviGlitch::Base.surely_formatted? path_or_object
22
+ raise 'Unsupported file passed.'
24
23
  end
24
+ @avi = Avi.new
25
+ @avi.tmpdir = tmpdir
26
+ @avi.path = path_or_object
25
27
  end
26
-
27
- unless AviGlitch::Base.surely_formatted? @file
28
- raise 'Unsupported file passed.'
29
- end
30
- @frames = Frames.new @file
28
+ @frames = Frames.new @avi
31
29
  end
32
30
 
33
31
  ##
34
32
  # Outputs the glitched file to +path+, and close the file.
35
33
  def output path, do_file_close = true
36
- FileUtils.cp @file.path, path
34
+ @avi.output path
37
35
  close if do_file_close
38
36
  self
39
37
  end
@@ -41,7 +39,7 @@ module AviGlitch
41
39
  ##
42
40
  # An explicit file close.
43
41
  def close
44
- @file.close!
42
+ @avi.close
45
43
  end
46
44
 
47
45
  ##
@@ -62,7 +60,7 @@ module AviGlitch
62
60
  def glitch target = :all, &block # :yield: data
63
61
  if block_given?
64
62
  @frames.each do |frame|
65
- if valid_target? target, frame
63
+ if frame.is? target
66
64
  frame.data = yield frame.data
67
65
  end
68
66
  end
@@ -127,55 +125,29 @@ module AviGlitch
127
125
  alias_method :write, :output
128
126
  alias_method :has_keyframes?, :has_keyframe?
129
127
 
130
- def valid_target? target, frame #:nodoc:
131
- return true if target == :all
132
- begin
133
- frame.send "is_#{target.to_s.sub(/frames$/, 'frame')}?"
134
- rescue
135
- false
136
- end
137
- end
138
-
139
- private :valid_target?
140
-
141
128
  class << self
142
129
  ##
143
130
  # Checks if the +file+ is a correctly formetted AVI file.
144
131
  # +file+ can be String or Pathname or IO.
145
132
  def surely_formatted? file, debug = false
146
- answer = true
147
- is_io = file.respond_to?(:seek) # Probably IO.
148
- file = File.open(file, 'rb') unless is_io
133
+ passed = true
149
134
  begin
150
- file.rewind
151
- unless file.read(4) == 'RIFF'
152
- answer = false
153
- warn 'RIFF sign is not found' if debug
154
- end
155
- len = file.read(4).unpack('V').first
156
- unless file.read(4) == 'AVI '
157
- answer = false
158
- warn 'AVI sign is not found' if debug
159
- end
160
- while file.read(4) =~ /^(?:LIST|JUNK)$/ do
161
- s = file.read(4).unpack('V').first
162
- file.pos += s
163
- end
164
- file.pos -= 4
165
- # we require idx1
166
- unless file.read(4) == 'idx1'
167
- answer = false
168
- warn 'idx1 is not found' if debug
135
+ riff = Avi.rifftree file
136
+ {
137
+ 'RIFF-AVI sign': /^RIFF \(\d+\) ’AVI ’$/,
138
+ 'movi': /^\s+LIST \(\d+\) ’movi’$/,
139
+ 'idx1': /^\s+idx1 \(\d+\)$/
140
+ }.each do |m, r|
141
+ unless riff =~ r
142
+ warn "#{m} is not found." if debug
143
+ passed = false
144
+ end
169
145
  end
170
- s = file.read(4).unpack('V').first
171
- file.pos += s
172
- rescue => err
173
- warn err.message if debug
174
- answer = false
175
- ensure
176
- file.close unless is_io
146
+ rescue => e
147
+ warn e.message if debug
148
+ passed = false
177
149
  end
178
- answer
150
+ passed
179
151
  end
180
152
  end
181
153
  end