aviglitch 0.1.6 → 0.2.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.
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