sequencer 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/.autotest ADDED
@@ -0,0 +1,23 @@
1
+ # -*- ruby -*-
2
+
3
+ require 'autotest/restart'
4
+
5
+ # Autotest.add_hook :initialize do |at|
6
+ # at.extra_files << "../some/external/dependency.rb"
7
+ #
8
+ # at.libs << ":../some/external"
9
+ #
10
+ # at.add_exception 'vendor'
11
+ #
12
+ # at.add_mapping(/dependency.rb/) do |f, _|
13
+ # at.files_matching(/test_.*rb$/)
14
+ # end
15
+ #
16
+ # %w(TestA TestB).each do |klass|
17
+ # at.extra_class_map[klass] = "test/test_misc.rb"
18
+ # end
19
+ # end
20
+
21
+ # Autotest.add_hook :run_command do |at|
22
+ # system "rake build"
23
+ # end
data/History.txt ADDED
@@ -0,0 +1,6 @@
1
+ === 1.0.0 / 2010-02-10
2
+
3
+ * 1 major enhancement
4
+
5
+ * Birthday!
6
+
data/Manifest.txt ADDED
@@ -0,0 +1,8 @@
1
+ .autotest
2
+ History.txt
3
+ Manifest.txt
4
+ README.txt
5
+ Rakefile
6
+ bin/seqls
7
+ lib/sequencer.rb
8
+ test/test_sequencer.rb
data/README.txt ADDED
@@ -0,0 +1,49 @@
1
+ = sequencer
2
+
3
+ * http://guerilla-di.org/sequencer
4
+
5
+ == DESCRIPTION:
6
+
7
+ Simplifies working with image sequences
8
+
9
+ == FEATURES/PROBLEMS:
10
+
11
+ * List all sequences in a directory interspersed with other file entries
12
+ * Detect sequence from single file
13
+
14
+ == SYNOPSIS:
15
+
16
+ require "sequencer"
17
+ s = Sequencer.from_single_file("/RAID/Film/CONFORM.092183.dpx")
18
+ s.file_count #=> 3201
19
+ s.gaps? #=> true
20
+ s.missing_frames #=> 15, somebody was careless
21
+
22
+ == INSTALL:
23
+
24
+ * sudo gem install sequencer
25
+
26
+ == LICENSE:
27
+
28
+ (The MIT License)
29
+
30
+ Copyright (c) 2010 Julik Tarkhanov (me@julik.nl)
31
+
32
+ Permission is hereby granted, free of charge, to any person obtaining
33
+ a copy of this software and associated documentation files (the
34
+ 'Software'), to deal in the Software without restriction, including
35
+ without limitation the rights to use, copy, modify, merge, publish,
36
+ distribute, sublicense, and/or sell copies of the Software, and to
37
+ permit persons to whom the Software is furnished to do so, subject to
38
+ the following conditions:
39
+
40
+ The above copyright notice and this permission notice shall be
41
+ included in all copies or substantial portions of the Software.
42
+
43
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
44
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
45
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
46
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
47
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
48
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
49
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ # -*- ruby -*-
2
+ require 'rubygems'
3
+ require 'hoe'
4
+ require File.dirname(__FILE__) + "/lib/sequencer"
5
+
6
+ Hoe.spec 'sequencer' do | s |
7
+ s.version = Sequencer::VERSION
8
+ s.developer('Julik Tarkhanov', 'me@julik.nl')
9
+ s.extra_dev_deps = {"test-spec" => ">=0"}
10
+ end
11
+
12
+ # vim: syntax=ruby
data/bin/seqls ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require File.dirname(__FILE__) + "/../lib/sequencer"
4
+ d = ARGV.shift || Dir.pwd
5
+
6
+ Sequencer.entries(d).each do | e |
7
+ puts e
8
+ end
data/lib/sequencer.rb ADDED
@@ -0,0 +1,208 @@
1
+ module Sequencer
2
+ VERSION = '1.0.0'
3
+ NUMBERS_AT_END = /(\d+)([^\d]+)?$/
4
+
5
+ extend self
6
+
7
+ # Detects sequences in the passed directory (same as Dir.entries but returns Sequence objects).
8
+ # Single files will be upgraded to single-frame Sequences
9
+ def entries(of_dir)
10
+ actual_files = Dir.entries(of_dir)[2..-1]
11
+ groups = {}
12
+
13
+ actual_files.each do | e |
14
+ if e =~ NUMBERS_AT_END
15
+ base = e[0...-([$1, $2].join.length)]
16
+ key = [base, $2]
17
+ groups[key] ||= []
18
+ groups[key] << e
19
+ else
20
+ groups[e] = [e]
21
+ end
22
+ end
23
+
24
+ groups.map do | key, filenames |
25
+ Sequence.new(of_dir, filenames)
26
+ end
27
+ end
28
+
29
+ # Detect a Sequence from a single file and return a handle to it
30
+ def from_single_file(path_to_single_file)
31
+ File.stat(path_to_single_file)
32
+ frame_number = path_to_single_file.scan(NUMBERS_AT_END).flatten.shift
33
+ if frame_number =~ /^0/ # Assume that the input is padded and take the glob path
34
+ sequence_via_glob(path_to_single_file)
35
+ else # Take the slower path by pattern-matching on entries
36
+ sequence_via_patterns(path_to_single_file)
37
+ end
38
+ end
39
+
40
+ # Get a glob pattern and padding offset for a file in a sequence
41
+ def glob_and_padding_for(path)
42
+ plen = 0
43
+ glob_pattern = path.gsub(NUMBERS_AT_END) do
44
+ plen = $1.length
45
+ ('[0-9]' * plen) + $2.to_s
46
+ end
47
+ return nil if glob_pattern == path
48
+
49
+ [glob_pattern, plen]
50
+ end
51
+
52
+ private
53
+
54
+ def sequence_via_glob(path_to_single_file)
55
+ glob, padding = glob_and_padding_for(path_to_single_file)
56
+ seq_glob = File.join(File.dirname(path_to_single_file), File.basename(glob))
57
+ files = Dir.glob(seq_glob).map {|f| File.basename(f) }
58
+
59
+ Sequence.new(File.expand_path(File.dirname(path_to_single_file)), files)
60
+ end
61
+
62
+ def sequence_via_patterns(path_to_single_file)
63
+ base_glob_pattern = path_to_single_file.gsub(NUMBERS_AT_END, '*')
64
+ closing_element = $2
65
+ matching_paths = Dir.glob(base_glob_pattern).select do | p |
66
+ number, closer = p.scan(NUMBERS_AT_END).flatten
67
+ closer == closing_element
68
+ end
69
+ files = matching_paths.map {|f| File.basename(f) }
70
+ Sequence.new(File.expand_path(File.dirname(path_to_single_file)), files)
71
+ end
72
+
73
+ public
74
+
75
+ class Sequence
76
+ include Enumerable
77
+ attr_reader :pattern
78
+
79
+ def initialize(directory, filenames)
80
+ raise "Can't sequence nothingness" if filenames.empty?
81
+ @directory, @filenames = directory, natural_sort(filenames)
82
+ @directory.freeze
83
+ @filenames.freeze
84
+ detect_gaps!
85
+ detect_pattern!
86
+ end
87
+
88
+ # Returns true if the files in the sequence can have numbers
89
+ def numbered?
90
+ @numbered ||= !!(@filenames[0] =~ NUMBERS_AT_END)
91
+ end
92
+
93
+ # Returns true if this sequence has gaps
94
+ def gaps?
95
+ @ranges.length > 1
96
+ end
97
+
98
+ # Tells whether this is a single frame sequence
99
+ def single_file?
100
+ @filenames.length == 1
101
+ end
102
+
103
+ def inspect
104
+ '#<%s>' % to_s
105
+ end
106
+
107
+ def to_s
108
+ return @filenames[0] if (!numbered? || single_file?)
109
+
110
+ printable = unless single_file?
111
+ @ranges.map do | r |
112
+ "%d..%d" % [r.begin, r.end]
113
+ end.join(', ')
114
+ else
115
+ @ranges[0].begin
116
+ end
117
+ @inspect_pattern % "[#{printable}]"
118
+ end
119
+
120
+ def expected_frames
121
+ @expected_frames ||= ((@ranges[-1].end - @ranges[0].begin) + 1)
122
+ end
123
+
124
+ def gap_count
125
+ @ranges.length - 1
126
+ end
127
+
128
+ # Returns the number of frames that the sequence should contain to be continuous
129
+ def missing_frames
130
+ expected_frames - file_count
131
+ end
132
+
133
+ # Returns the actual file count in the sequence
134
+ def file_count
135
+ @file_count ||= @filenames.length
136
+ end
137
+
138
+ # Check if this sequencer includes a file
139
+ def include?(base_filename)
140
+ @filenames.include?(base_filename)
141
+ end
142
+
143
+ # Yield each filename in the sequence to the block
144
+ def each
145
+ @filenames.each {|f| yield(f) }
146
+ end
147
+
148
+ # Yield each absolute path to a file in the sequence to the block
149
+ def each_path
150
+ @filenames.each{|f| yield(File.join(@directory, f))}
151
+ end
152
+
153
+ private
154
+
155
+ def natural_sort(ar)
156
+ ar.sort_by {|e| e.scan(NUMBERS_AT_END).flatten.shift.to_i }
157
+ end
158
+
159
+ def detect_pattern!
160
+
161
+ unless numbered?
162
+ @inspect_pattern = "%s"
163
+ @pattern = @filenames[0]
164
+ else
165
+ @inspect_pattern = @filenames[0].gsub(NUMBERS_AT_END) do
166
+ ["%s", $2].join
167
+ end
168
+
169
+ highest_padding = nil
170
+ @pattern = @filenames[-1].gsub(NUMBERS_AT_END) do
171
+ highest_padding = $1.length
172
+ ["%0#{$1.length}d", $2].join
173
+ end
174
+
175
+ # Look at the first file in the sequence. If it has a lesser number of
176
+ lowest_padding = @filenames[0].scan(NUMBERS_AT_END).flatten.shift.length
177
+ if lowest_padding < highest_padding # Natural numbering
178
+ @pattern = @filenames[0].gsub(NUMBERS_AT_END) do
179
+ ["%d", $2].join
180
+ end
181
+ end
182
+ end
183
+
184
+ @inspect_pattern.freeze
185
+ @pattern.freeze
186
+ end
187
+
188
+ def detect_gaps!
189
+ only_numbers = @filenames.map do | f |
190
+ f.scan(NUMBERS_AT_END).flatten.shift.to_i
191
+ end
192
+ @ranges = to_ranges(only_numbers)
193
+ end
194
+
195
+ def to_ranges(array)
196
+ array.compact.sort.uniq.inject([]) do | result, elem |
197
+ result = [elem..elem] if result.length.zero?
198
+ if [result[-1].end, result[-1].end.succ].include?(elem)
199
+ result[-1] = result[-1].begin..elem
200
+ else
201
+ result.push(elem..elem)
202
+ end
203
+ result
204
+ end
205
+ end
206
+ end
207
+
208
+ end
@@ -0,0 +1,156 @@
1
+ require 'test/unit'
2
+ require 'rubygems'
3
+ require 'test/spec'
4
+ require 'fileutils'
5
+ require File.dirname(__FILE__) + '/../lib/sequencer'
6
+
7
+ TEST_DIR = File.dirname(__FILE__) + "/tmp"
8
+
9
+ def emit_test_dirs
10
+ start_f = 123
11
+ end_f = 568
12
+
13
+ FileUtils.mkdir_p(TEST_DIR + "/sequence_and_sole_file")
14
+ main_seq = TEST_DIR + "/sequence_and_sole_file/seq1.%06d.tif"
15
+ sole_file = TEST_DIR + "/sequence_and_sole_file/somefile.tif"
16
+ (start_f..end_f).each do | i |
17
+ FileUtils.touch(main_seq % i)
18
+ end
19
+ FileUtils.touch(sole_file)
20
+
21
+ broken_seq = TEST_DIR + "/sequence_and_sole_file/broken_seq.%06d.tif"
22
+
23
+ (start_f..end_f).each do | i |
24
+ FileUtils.touch(broken_seq % i)
25
+ end
26
+
27
+ ((end_f + 10)..(end_f + 134)).each do | i |
28
+ FileUtils.touch(broken_seq % i)
29
+ end
30
+
31
+ single_file_seq = TEST_DIR + "/sequence_and_sole_file/single_file.002123154.tif"
32
+ FileUtils.touch(single_file_seq)
33
+
34
+ FileUtils.mkdir_p(TEST_DIR + "/natural_numbering")
35
+ nat_seq = TEST_DIR + "/natural_numbering/somefiles %d.png"
36
+
37
+ (5545142...5545172).each do | i |
38
+ FileUtils.touch(nat_seq % i)
39
+ end
40
+
41
+ FileUtils.mkdir_p(TEST_DIR + "/many_seqs")
42
+ (458..512).each do | i |
43
+ FileUtils.touch(TEST_DIR + "/many_seqs/seq1.%d.tif" % i)
44
+ end
45
+
46
+ (228..312).each do | i |
47
+ FileUtils.touch(TEST_DIR + "/many_seqs/anotherS %d.tif" % i)
48
+ end
49
+ FileUtils.touch(TEST_DIR + "/many_seqs/single.tif")
50
+ $emitted = true
51
+ end
52
+
53
+ def teardown_test_dirs
54
+ FileUtils.rm_rf(TEST_DIR)
55
+ end
56
+
57
+ context "Sequencer.glob_and_padding_for should" do
58
+
59
+ specify "return proper glob pattern and padding for a path with extension" do
60
+ glob, pad = Sequencer.glob_and_padding_for("file.00001.gif")
61
+ glob.should.equal "file.[0-9][0-9][0-9][0-9][0-9].gif"
62
+ pad.should.equal 5
63
+ end
64
+
65
+ specify "return proper glob pattern and padding for a path without extension" do
66
+ glob, pad = Sequencer.glob_and_padding_for("file.00001")
67
+ glob.should.equal "file.[0-9][0-9][0-9][0-9][0-9]"
68
+ pad.should.equal 5
69
+ end
70
+
71
+ specify "return nil for a file that is not a sequence" do
72
+ glob, pad = Sequencer.glob_and_padding_for("file")
73
+ glob.should.be.nil
74
+ end
75
+ end
76
+
77
+ context "Sequencer.entries should" do
78
+ before { emit_test_dirs }
79
+ after { teardown_test_dirs }
80
+ specify "return entries for every sequence in a directory" do
81
+ entries = Sequencer.entries(TEST_DIR + "/many_seqs")
82
+ end
83
+ end
84
+
85
+ context "A Sequence created from unpadded files should" do
86
+ before { emit_test_dirs }
87
+ after { teardown_test_dirs }
88
+
89
+ specify "be properly created" do
90
+ s = Sequencer.from_single_file(TEST_DIR + "/natural_numbering/somefiles 5545168.png")
91
+ s.should.not.be.nil
92
+ s.expected_frames.should.equal 30
93
+ s.file_count.should.equal 30
94
+ s.pattern.should.equal "somefiles %07d.png"
95
+ end
96
+ end
97
+
98
+ context "A Sequence created from a file that has no numbering slot should" do
99
+ before { @single = Sequencer::Sequence.new("/tmp", ["foo.tif"]) }
100
+
101
+ specify "report a pattern that is the same as filename" do
102
+ @single.pattern.should.equal "foo.tif"
103
+ @single.should.be.single_file?
104
+ @single.expected_frames.should.equal 1
105
+ @single.inspect.should.equal "#<foo.tif>"
106
+ end
107
+ end
108
+
109
+ context "A Sequence created from pad-numbered files should" do
110
+ before do
111
+ emit_test_dirs
112
+ @gapless = Sequencer.from_single_file(TEST_DIR + "/sequence_and_sole_file/seq1.000245.tif")
113
+ @with_gaps = Sequencer.from_single_file(TEST_DIR + "/sequence_and_sole_file/broken_seq.000245.tif")
114
+ @single = Sequencer.from_single_file(TEST_DIR + "/sequence_and_sole_file/single_file.002123154.tif")
115
+ end
116
+
117
+ after { teardown_test_dirs }
118
+
119
+ specify "initialize itself from one path to a file in the sequence without gaps" do
120
+ @gapless.should.not.be.nil
121
+ @gapless.should.be.kind_of(Sequencer::Sequence)
122
+ @gapless.should.respond_to(:gaps?)
123
+ @gapless.should.not.be.single_file?
124
+
125
+ @gapless.should.blaming("this is a gapless sequence").not.be.gaps?
126
+ @gapless.file_count.should.blaming("actual file count in sequence").equal(446)
127
+ @gapless.expected_frames.should.blaming("expected frame count in sequence").equal(446)
128
+ @gapless.inspect.should.blaming("inspect itself").equal('#<seq1.[123..568].tif>')
129
+ @gapless.pattern.should.equal 'seq1.%06d.tif'
130
+ files = @gapless.map {|f| f }
131
+ files.length.should.equal 446
132
+ files[0].should.equal 'seq1.000123.tif'
133
+ end
134
+
135
+ specify "initialize itself from one path to a file in the sequence with gaps" do
136
+ @with_gaps.should.not.be.nil
137
+ @with_gaps.should.be.kind_of(Sequencer::Sequence)
138
+ @with_gaps.should.not.be.single_file?
139
+
140
+ @with_gaps.should.be.gaps?
141
+ @with_gaps.gap_count.should.equal 1
142
+ @with_gaps.missing_frames.should.equal(9)
143
+ @with_gaps.inspect.should.blaming("inspect itself").equal('#<broken_seq.[123..568, 578..702].tif>')
144
+ @with_gaps.should.include("broken_seq.000123.tif")
145
+ @with_gaps.should.not.include("bogus.123.tif")
146
+ end
147
+
148
+ specify "initialize itself from a single file" do
149
+ @single.should.be.single_file?
150
+ @single.inspect.should.equal '#<single_file.002123154.tif>'
151
+ @single.should.not.be.gaps?
152
+ @single.expected_frames.should.equal 1
153
+ @single.file_count.should.equal 1
154
+ end
155
+
156
+ end
metadata ADDED
@@ -0,0 +1,85 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: sequencer
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Julik Tarkhanov
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2010-02-13 00:00:00 +01:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: test-spec
17
+ type: :development
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: "0"
24
+ version:
25
+ - !ruby/object:Gem::Dependency
26
+ name: hoe
27
+ type: :development
28
+ version_requirement:
29
+ version_requirements: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: 2.3.3
34
+ version:
35
+ description: Simplifies working with image sequences
36
+ email:
37
+ - me@julik.nl
38
+ executables:
39
+ - seqls
40
+ extensions: []
41
+
42
+ extra_rdoc_files:
43
+ - History.txt
44
+ - Manifest.txt
45
+ - README.txt
46
+ files:
47
+ - .autotest
48
+ - History.txt
49
+ - Manifest.txt
50
+ - README.txt
51
+ - Rakefile
52
+ - bin/seqls
53
+ - lib/sequencer.rb
54
+ - test/test_sequencer.rb
55
+ has_rdoc: true
56
+ homepage: http://guerilla-di.org/sequencer
57
+ licenses: []
58
+
59
+ post_install_message:
60
+ rdoc_options:
61
+ - --main
62
+ - README.txt
63
+ require_paths:
64
+ - lib
65
+ required_ruby_version: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - ">="
68
+ - !ruby/object:Gem::Version
69
+ version: "0"
70
+ version:
71
+ required_rubygems_version: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: "0"
76
+ version:
77
+ requirements: []
78
+
79
+ rubyforge_project: sequencer
80
+ rubygems_version: 1.3.5
81
+ signing_key:
82
+ specification_version: 3
83
+ summary: Simplifies working with image sequences
84
+ test_files:
85
+ - test/test_sequencer.rb