aviglitch 0.1.6 → 0.2.2

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