sequencer 1.0.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.
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