aviglitch 0.1.6 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 67b4c434f616f3731fd09f5b0d0263e634cc87148e8fad42dd38e6b4d554222a
4
- data.tar.gz: a61ad04a958aec548dec7b554ec8f1b24ff5973d4fa76862dc0ca067fccbeb11
3
+ metadata.gz: 6bcaf49d8305bd36386f606954fe702468fc5196e30b0d5e4f3dea11f0bfb8a7
4
+ data.tar.gz: a1ede07ce50e3005abcd505e501ddf8f73c18b0241d7b34834ac78781d76e389
5
5
  SHA512:
6
- metadata.gz: 20b11703e84322c798b7a450508d9848f0dd25534ff9bbf3395b269dd8959a454ad805d013dfcb3cdffadccd2c120b8a4912e1356cfa9079c7a3791284fa9bad
7
- data.tar.gz: 9c5dd3b8c13a186a59b5fd4ad97c992e24c2967dd4629d86b1c6678c0b2fd07ad66a9711175dc499ec6e83893caa464fce2fa374e60d6daf79b47063e3fcf039
6
+ metadata.gz: b9a0c0905f0131bec187618e48e7281eac79d01d6067a66dad82bafbc2c923d84bd7472738b147fc2866ee1f5ef14b1064556e05b7109a7ffef5a0c73370ec6f
7
+ data.tar.gz: f698216510d97cf7713e14127cf75af8264c5c003fa876caed72e51168575eb75e0dba05930ff22c6a145c0da9a0967da9959dfc65d0276909e9db081ba6810c
data/.gitignore CHANGED
@@ -9,3 +9,4 @@ tmp
9
9
  Gemfile.lock
10
10
  Guardfile
11
11
  .yardoc
12
+ spec/files
data/ChangeLog.md CHANGED
@@ -1,3 +1,11 @@
1
+ ### 0.2.0 / 2021-09-13
2
+
3
+ * Support for AVI2.0 formatted files. Now this library can handle files larger than 1GB.
4
+ * Added methods to Frames, including #index, #rindex, #first_of, and #last_of
5
+ * Removed warnings for the file size getting large.
6
+ * Added the class AviGlitch::Avi which manages binary RIFF-AVI data.
7
+ * A lot of internal changes.
8
+
1
9
  ### 0.1.6 / 2021-08-21
2
10
 
3
11
  * Removed obsolete dependencies.
data/Gemfile CHANGED
@@ -1,2 +1,7 @@
1
1
  source "https://rubygems.org"
2
2
  gemspec
3
+
4
+ group :test do
5
+ gem 'guard'
6
+ gem 'guard-rspec'
7
+ end
data/README.md CHANGED
@@ -44,10 +44,6 @@ For more practical usages, please check <https://github.com/ucnv/aviglitch-utils
44
44
  gem install aviglitch
45
45
  ```
46
46
 
47
- ## Known issues
48
-
49
- - This library doesn't support AVI2 format spec. This means that it will not work as expected for files larger than 1GB.
50
-
51
47
  ## License
52
48
 
53
49
  This library is distributed under the terms and conditions of the [MIT license](LICENSE).
data/aviglitch.gemspec CHANGED
@@ -10,7 +10,7 @@ Gem::Specification.new do |spec|
10
10
  spec.email = ["ucnvvv@gmail.com"]
11
11
  spec.summary = %q{A Ruby library to destroy your AVI files.}
12
12
  spec.description = spec.summary
13
- spec.homepage = "http://ucnv.github.com/aviglitch/"
13
+ spec.homepage = "https://github.com/ucnv/aviglitch"
14
14
  spec.license = "MIT"
15
15
 
16
16
  spec.files = `git ls-files`.split($/)
@@ -18,7 +18,6 @@ Gem::Specification.new do |spec|
18
18
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
19
  spec.require_paths = ["lib"]
20
20
 
21
- spec.has_rdoc = true
22
21
  spec.extra_rdoc_files = ["README.md", "LICENSE"]
23
22
  spec.rdoc_options << "-m" << "README.md"
24
23
 
data/bin/datamosh CHANGED
@@ -54,7 +54,12 @@ unless fake
54
54
  (!all && i == 0) ? frame : "" # keep the first frame
55
55
  end
56
56
  end
57
- a.mutate_keyframes_into_deltaframes!(!all && !fake ? 1..a.frames.size : nil)
57
+ if !all && !fake
58
+ first = a.frames.index(a.frames.first_of(:keyframe))
59
+ a.mutate_keyframes_into_deltaframes! (first + 1)..a.frames.size
60
+ else
61
+ a.mutate_keyframes_into_deltaframes!
62
+ end
58
63
 
59
64
  input.each do |file|
60
65
  b = AviGlitch.open file
@@ -0,0 +1,550 @@
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 #:nodoc:
83
+ protected :path, :path=, :movi, :movi=
84
+
85
+ ##
86
+ # Generates an instance with the necessary structure from the +path+.
87
+ def initialize path = nil
88
+ return if path.nil?
89
+ @path = path
90
+ File.open(path, 'rb') do |io|
91
+ @movi = Tempfile.new 'aviglitch', binmode: true
92
+ @riff = []
93
+ @indices = []
94
+ @superidx = []
95
+ @was_avi2 = false
96
+ io.rewind
97
+ parse_riff io, @riff
98
+ if was_avi2?
99
+ @indices.sort_by! { |ix| ix[:offset] }
100
+ end
101
+ end
102
+ end
103
+
104
+ ##
105
+ # Parses the passed RIFF formated file recursively.
106
+ def parse_riff io, target, len = 0, is_movi = false
107
+ offset = io.pos
108
+ binoffset = @movi.pos
109
+ while id = io.read(4) do
110
+ if len > 0 && io.pos >= offset + len
111
+ io.pos -= 4
112
+ break
113
+ end
114
+ size = io.read(4).unpack('V').first
115
+ if id == 'RIFF' || id == 'LIST'
116
+ lid = io.read(4)
117
+ newarr = []
118
+ chunk = RiffChunk.new id, size, lid, newarr
119
+ target << chunk
120
+ parse_riff io, newarr, size, lid == 'movi'
121
+ else
122
+ value = nil
123
+ if is_movi
124
+ if id =~ /^ix/
125
+ v = io.read size
126
+ # confirm the super index surely has information
127
+ @superidx.each do |sidx|
128
+ nent = sidx[4, 4].unpack('v').first
129
+ cid = sidx[8, 4]
130
+ nent.times do |i|
131
+ ent = sidx[24 + 16 * i, 16]
132
+ # we can check other informations thuogh
133
+ valid = ent[0, 8].unpack('q').first == io.pos - v.size - 8
134
+ parse_avi2_indices(v, binoffset) if valid
135
+ end
136
+ end
137
+ else
138
+ io.pos -= 8
139
+ v = io.read(size + 8)
140
+ @movi.print v
141
+ @movi.print "\0" if size % 2 == 1
142
+ end
143
+ elsif id == 'idx1'
144
+ v = io.read size
145
+ parse_avi1_indices v unless was_avi2?
146
+ else
147
+ value = io.read size
148
+ if id == 'indx'
149
+ @superidx << value
150
+ @was_avi2 = true
151
+ end
152
+ end
153
+ chunk = RiffChunk.new id, size, value
154
+ target << chunk
155
+ io.pos += 1 if size % 2 == 1
156
+ end
157
+ end
158
+ end
159
+
160
+ ##
161
+ # Closes the file.
162
+ def close
163
+ @movi.close!
164
+ end
165
+
166
+ ##
167
+ # Detects the passed file was an AVI2.0 file.
168
+ def was_avi2?
169
+ @was_avi2
170
+ end
171
+
172
+ ##
173
+ # Detects the current data will be an AVI2.0 file.
174
+ def is_avi2?
175
+ @movi.size >= MAX_RIFF_SIZE
176
+ end
177
+
178
+ ##
179
+ # Saves data to AVI formatted file.
180
+ def output path
181
+ @index_pos = 0
182
+ # prepare headers by reusing existing ones
183
+ strl = search 'hdrl', 'strl'
184
+ if is_avi2?
185
+ # indx
186
+ vid_frames_size = 0
187
+ @indexinfo = @indices.collect { |ix|
188
+ vid_frames_size += 1 if ix[:id] =~ /d[bc]$/
189
+ ix[:id]
190
+ }.uniq.sort.collect { |d|
191
+ [d, {}]
192
+ }.to_h # should be like: {"00dc"=>{}, "01wb"=>{}}
193
+ strl.each_with_index do |sl, i|
194
+ indx = sl.child 'indx'
195
+ if indx.nil?
196
+ indx = RiffChunk.new('indx', 4120, "\0" * 4120)
197
+ indx.value[0, 8] = [4, 0, 0, 0].pack('vccV')
198
+ sl.value.push indx
199
+ else
200
+ indx.value[4, 4] = [0].pack('V')
201
+ indx.value[24..-1] = "\0" * (indx.value.size - 24)
202
+ end
203
+ preid = indx.value[8, 4]
204
+ info = @indexinfo.find do |key, val|
205
+ # more strict way must exist though..
206
+ if preid == "\0\0\0\0"
207
+ key.start_with? "%02d" % i
208
+ else
209
+ key == preid
210
+ end
211
+ end
212
+ indx.value[8, 4] = info.first if preid == "\0\0\0\0"
213
+ info.last[:indx] = indx
214
+ info.last[:fcc] = 'ix' + info.first[0, 2]
215
+ info.last[:cur] = []
216
+ end
217
+ # odml
218
+ odml = search('hdrl', 'odml').first
219
+ if odml.nil?
220
+ odml = RiffChunk.new(
221
+ 'LIST', 260, 'odml', [RiffChunk.new('dmlh', 248, "\0" * 248)]
222
+ )
223
+ @riff.first.child('hdrl').value.push odml
224
+ end
225
+ odml.child('dmlh').value[0, 4] = [@indices.size].pack('V')
226
+ else
227
+ strl.each do |sl|
228
+ indx = sl.child 'indx'
229
+ unless indx.nil?
230
+ sl.value.delete indx
231
+ end
232
+ end
233
+ end
234
+
235
+ # movi
236
+ write_movi = ->(io) do
237
+ vid_frames_size = 0
238
+ io.print 'LIST'
239
+ io.print "\0\0\0\0"
240
+ data_offset = io.pos
241
+ io.print 'movi'
242
+ while io.pos - data_offset <= MAX_RIFF_SIZE
243
+ ix = @indices[@index_pos]
244
+ @indexinfo[ix[:id]][:cur] << {
245
+ pos: io.pos, size: ix[:size], flag: ix[:flag]
246
+ } if is_avi2?
247
+ io.print ix[:id]
248
+ vid_frames_size += 1 if ix[:id] =~ /d[bc]$/
249
+ io.print [ix[:size]].pack('V')
250
+ @movi.pos += 8
251
+ io.print @movi.read(ix[:size])
252
+ if ix[:size] % 2 == 1
253
+ io.print "\0"
254
+ @movi.pos += 1
255
+ end
256
+ @index_pos += 1
257
+ break if @index_pos > @indices.size - 1
258
+ end
259
+ # standard index
260
+ if is_avi2?
261
+ @indexinfo.each do |key, info|
262
+ ix_offset = io.pos
263
+ io.print info[:fcc]
264
+ io.print [24 + 8 * info[:cur].size].pack('V')
265
+ io.print [2, 0, 1, info[:cur].size].pack('vccV')
266
+ io.print key
267
+ io.print [data_offset, 0].pack('qV')
268
+ info[:cur].each.with_index do |cur, i|
269
+ io.print [cur[:pos] - data_offset + 8].pack('V') # 8 for LIST####
270
+ sz = cur[:size]
271
+ if cur[:flag] & Frame::AVIIF_KEYFRAME == 0 # is not keyframe
272
+ sz = sz | 0b1000_0000_0000_0000_0000_0000_0000_0000
273
+ end
274
+ io.print [sz].pack('V')
275
+ end
276
+ # rewrite indx
277
+ indx = info[:indx]
278
+ nent = indx.value[4, 4].unpack('V').first + 1
279
+ indx.value[4, 4] = [nent].pack('V')
280
+ indx.value[24 + 16 * (nent - 1), 16] = [
281
+ ix_offset, io.pos - ix_offset, info[:cur].size
282
+ ].pack('qVV')
283
+ io.pos = expected_position_of(indx) + 8
284
+ io.print indx.value
285
+ # clean up
286
+ info[:cur] = []
287
+ io.seek 0, IO::SEEK_END
288
+ end
289
+ end
290
+ # size of movi
291
+ size = io.pos - data_offset
292
+ io.pos = data_offset - 4
293
+ io.print [size].pack('V')
294
+ io.seek 0, IO::SEEK_END
295
+ io.print "\0" if size % 2 == 1
296
+ vid_frames_size
297
+ end
298
+
299
+ File.open(path, 'w+') do |io|
300
+ io.binmode
301
+ @movi.rewind
302
+ # normal AVI
303
+ # header
304
+ io.print 'RIFF'
305
+ io.print "\0\0\0\0"
306
+ io.print 'AVI '
307
+ @riff.first.value.each do |chunk|
308
+ break if chunk.id == 'movi'
309
+ print_chunk io, chunk
310
+ end
311
+ # movi
312
+ vid_size = write_movi.call io
313
+ # rewrite frame count in avih header
314
+ io.pos = 48
315
+ io.print [vid_size].pack('V')
316
+ io.seek 0, IO::SEEK_END
317
+ # idx1
318
+ io.print 'idx1'
319
+ io.print [@index_pos * 16].pack('V')
320
+ @indices[0..(@index_pos - 1)].each do |ix|
321
+ io.print ix[:id] + [ix[:flag], ix[:offset] + 4, ix[:size]].pack('V3')
322
+ end
323
+ # rewrite riff chunk size
324
+ avisize = io.pos - 8
325
+ io.pos = 4
326
+ io.print [avisize].pack('V')
327
+ io.seek 0, IO::SEEK_END
328
+
329
+ # AVI2.0
330
+ while @index_pos < @indices.size
331
+ io.print 'RIFF'
332
+ io.print "\0\0\0\0"
333
+ riff_offset = io.pos
334
+ io.print 'AVIX'
335
+ # movi
336
+ write_movi.call io
337
+ # rewrite total chunk size
338
+ avisize = io.pos - riff_offset
339
+ io.pos = riff_offset - 4
340
+ io.print [avisize].pack('V')
341
+ io.seek 0, IO::SEEK_END
342
+ end
343
+ end
344
+ end
345
+
346
+ ##
347
+ # Provides internal accesses to movi binary data.
348
+ # It requires the yield block to return an array of pair values
349
+ # which consists of new indices array and new movi binary data.
350
+ def process_movi &block
351
+ @movi.rewind
352
+ newindices, newmovi = block.call @indices, @movi
353
+ unless @indices == newindices
354
+ @indices.replace newindices
355
+ end
356
+ unless @movi == newmovi
357
+ @movi.rewind
358
+ newmovi.rewind
359
+ while d = newmovi.read(BUFFER_SIZE) do
360
+ @movi.print d
361
+ end
362
+ eof = @movi.pos
363
+ @movi.truncate eof
364
+ end
365
+ end
366
+
367
+ ##
368
+ # Searches and returns RIFF values with the passed search +args+.
369
+ # +args+ should point the ids of the tree structured RIFF data
370
+ # under the 'AVI ' chunk without omission, like:
371
+ #
372
+ # avi.search 'hdrl', 'strl', 'indx'
373
+ #
374
+ # It returns a list of RiffChunk object which can be modified directly.
375
+ # (RiffChunk class which is returned through this method also has a #search
376
+ # method with the same interface as this class.)
377
+ # This method only seeks in the first RIFF 'AVI ' tree.
378
+ def search *args
379
+ @riff.first.search *args
380
+ end
381
+
382
+ ##
383
+ # Returns true if +other+'s indices are same as self's indices.
384
+ def == other
385
+ self.indices == other.indices
386
+ end
387
+
388
+ def inspect #:nodoc:
389
+ "#<#{self.class.name}:#{sprintf("0x%x", object_id)} @movi=#{@movi.inspect}>"
390
+ end
391
+
392
+ def initialize_copy avi #:nodoc:
393
+ avi.path = @path.dup
394
+ md = Marshal.dump @indices
395
+ avi.indices = Marshal.load md
396
+ md = Marshal.dump @riff
397
+ avi.riff = Marshal.load md
398
+ newmovi = Tempfile.new 'aviglitch', binmode: true
399
+ movipos = @movi.pos
400
+ @movi.rewind
401
+ newmovi.print @movi.read
402
+ @movi.pos = movipos
403
+ newmovi.rewind
404
+ avi.movi = newmovi
405
+ end
406
+
407
+ def print_chunk io, chunk #:nodoc:
408
+ offset = io.pos
409
+ if chunk.is_list?
410
+ io.print chunk.list
411
+ io.print "\0\0\0\0"
412
+ io.print chunk.id
413
+ chunk.value.each do |c|
414
+ print_chunk io, c
415
+ end
416
+ else
417
+ io.print chunk.id
418
+ io.print "\0\0\0\0"
419
+ io.print chunk.value
420
+ end
421
+ # rewrite size
422
+ size = io.pos - offset - 8
423
+ io.pos = offset + 4
424
+ io.print [size].pack('V')
425
+ io.seek 0, IO::SEEK_END
426
+ io.print "\0" if size % 2 == 1
427
+ end
428
+
429
+ def expected_position_of chunk #:nodoc:
430
+ pos = -1
431
+ cur = 12
432
+ seek = -> (chk) do
433
+ if chk === chunk
434
+ pos = cur
435
+ return
436
+ end
437
+ if chk.is_list?
438
+ cur += 12
439
+ chk.value.each do |c|
440
+ seek.call c
441
+ end
442
+ else
443
+ cur += 8
444
+ cur += chk.value.nil? ? chk.binsize : chk.value.size
445
+ end
446
+ end
447
+ headers = @riff.first.value
448
+ headers.each do |c|
449
+ seek.call c
450
+ end
451
+ pos
452
+ end
453
+
454
+ def parse_avi1_indices data #:nodoc:
455
+ # The function Frames#fix_offsets_if_needed in prev versions was now removed.
456
+ i = 0
457
+ while i * 16 < data.size do
458
+ @indices << {
459
+ :id => data[i * 16, 4],
460
+ :flag => data[i * 16 + 4, 4].unpack('V').first,
461
+ :offset => data[i * 16 + 8, 4].unpack('V').first - 4,
462
+ :size => data[i * 16 + 12, 4].unpack('V').first,
463
+ }
464
+ i += 1
465
+ end
466
+ end
467
+
468
+ def parse_avi2_indices data, offset #:nodoc:
469
+ id = data[8, 4]
470
+ nent = data[4, 4].unpack('V').first
471
+ h = 24
472
+ i = 0
473
+ while h + i * 8 < data.size
474
+ moffset = data[h + i * 8, 4].unpack('V').first
475
+ msize = data[h + i * 8 + 4, 4].unpack('V').first
476
+ of = offset + moffset - 12 # 12 for movi + 00dc####
477
+ # bit 31 is set if this is NOT a keyframe
478
+ fl = (msize >> 31 == 1) ? 0 : Frame::AVIIF_KEYFRAME
479
+ sz = msize & 0b0111_1111_1111_1111_1111_1111_1111_1111
480
+ @indices << {
481
+ :id => id,
482
+ :flag => fl,
483
+ :offset => of,
484
+ :size => sz,
485
+ }
486
+ i += 1
487
+ end
488
+ end
489
+
490
+ private :print_chunk, :expected_position_of,
491
+ :parse_avi1_indices, :parse_avi2_indices
492
+
493
+ class << self
494
+
495
+ ##
496
+ # Parses the +file+ and returns the RIFF structure.
497
+ def rifftree file, out = nil
498
+ returnable = out.nil?
499
+ out = StringIO.new if returnable
500
+
501
+ parse = ->(io, depth = 0, len = 0) do
502
+ offset = io.pos
503
+ while id = io.read(4) do
504
+ if len > 0 && io.pos >= offset + len
505
+ io.pos -= 4
506
+ break
507
+ end
508
+ size = io.read(4).unpack('V').first
509
+ str = depth > 0 ? ' ' * depth + id : id
510
+ if id =~ /^(?:RIFF|LIST)$/
511
+ lid = io.read(4)
512
+ str << (' (%d)' % size) + " ’#{lid}’\n"
513
+ out.print str
514
+ parse.call io, depth + 1, size
515
+ else
516
+ str << (' (%d)' % size ) + "\n"
517
+ out.print str
518
+ io.pos += size
519
+ io.pos += 1 if size % 2 == 1
520
+ end
521
+ end
522
+ end
523
+
524
+ io = file
525
+ is_io = file.respond_to?(:seek) # Probably IO.
526
+ io = File.open(file, 'rb') unless is_io
527
+ begin
528
+ io.rewind
529
+ parse.call io
530
+ io.rewind
531
+ ensure
532
+ io.close unless is_io
533
+ end
534
+
535
+ if returnable
536
+ out.rewind
537
+ out.read
538
+ end
539
+ end
540
+
541
+ ##
542
+ # Parses the +file+ and prints the RIFF structure to stdout.
543
+ def print_rifftree file
544
+ Avi.rifftree file, $stdout
545
+ end
546
+
547
+ end
548
+
549
+ end
550
+ end