subconv 0.1.0 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.gitignore +1 -1
- data/.gitlab-ci.yml +33 -0
- data/.rubocop.yml +26 -30
- data/Gemfile +2 -0
- data/Gemfile.lock +73 -0
- data/README.md +5 -8
- data/Rakefile +3 -1
- data/bin/subconv +7 -1
- data/lib/subconv.rb +2 -0
- data/lib/subconv/caption.rb +5 -0
- data/lib/subconv/caption_filter.rb +6 -4
- data/lib/subconv/scc/reader.rb +190 -62
- data/lib/subconv/scc/transformer.rb +64 -33
- data/lib/subconv/utility.rb +4 -1
- data/lib/subconv/version.rb +3 -1
- data/lib/subconv/webvtt/writer.rb +44 -33
- data/spec/.rubocop.yml +8 -0
- data/spec/caption_filter_spec.rb +2 -0
- data/spec/scc/reader_spec.rb +140 -65
- data/spec/scc/transformer_spec.rb +168 -48
- data/spec/spec_helper.rb +8 -2
- data/spec/test_helpers.rb +18 -0
- data/spec/webvtt/writer_spec.rb +16 -14
- data/subconv.gemspec +11 -11
- metadata +38 -35
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'subconv/utility'
|
2
4
|
require 'subconv/caption'
|
3
5
|
|
@@ -29,25 +31,18 @@ module Subconv
|
|
29
31
|
captions_open = []
|
30
32
|
end
|
31
33
|
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
))
|
44
|
-
end
|
34
|
+
# Collect text chunks in each row and create captions out of them
|
35
|
+
caption.grid&.each_with_index do |row, row_number|
|
36
|
+
chunks = collect_chunks(row)
|
37
|
+
chunks.each_pair do |start_column, chunk|
|
38
|
+
content = transform_chunk(chunk)
|
39
|
+
position = position_from_grid(row_number, start_column)
|
40
|
+
captions_open.push(Subconv::Caption.new(
|
41
|
+
align: :start,
|
42
|
+
position: position,
|
43
|
+
content: content
|
44
|
+
))
|
45
45
|
end
|
46
|
-
|
47
|
-
# Merge continuous rows?
|
48
|
-
# captions_open.each do |caption_open|
|
49
|
-
#
|
50
|
-
# end
|
51
46
|
end
|
52
47
|
|
53
48
|
last_time = caption.timecode
|
@@ -64,12 +59,41 @@ module Subconv
|
|
64
59
|
transformed_captions
|
65
60
|
end
|
66
61
|
|
62
|
+
# Transform paint-on captions to pop-on captions by combining successive captions that only add text.
|
63
|
+
# As soon as text is removed or text that is already on-screen is changed, a caption is produced.
|
64
|
+
def combine_paint_on_captions(captions)
|
65
|
+
first_paint_on_caption = nil
|
66
|
+
last_paint_on_caption = nil
|
67
|
+
# Insert nil pseudo-element at end for flushing
|
68
|
+
(captions + [nil]).each_with_object([]) do |caption, result_captions|
|
69
|
+
if caption&.paint_on_mode?
|
70
|
+
first_paint_on_caption ||= caption
|
71
|
+
# Detect when characters disappear/change; until then: skip all paint-on captions (that just add text).
|
72
|
+
# At the same time, always produce a cue for empty grids so explicit display clears do not get lost.
|
73
|
+
# Simple character replacement cues that replace standard characters with extended ones should also never trigger a new caption.
|
74
|
+
if !last_paint_on_caption.nil? && !caption.char_replacement? && (!require_grid_object(last_paint_on_caption.grid).without_identical_characters(require_grid_object(caption.grid)).empty? || last_paint_on_caption.grid.nil?)
|
75
|
+
# Take timecode from the first caption of the current batch, but the grid from the current caption
|
76
|
+
result_captions << Scc::Caption.new(timecode: first_paint_on_caption.timecode, grid: last_paint_on_caption.grid, mode: :pop_on)
|
77
|
+
# Caption produced, so this marks a new segment
|
78
|
+
first_paint_on_caption = caption
|
79
|
+
end
|
80
|
+
last_paint_on_caption = caption
|
81
|
+
else
|
82
|
+
# Flush out last paint-on caption if necessary
|
83
|
+
result_captions << Scc::Caption.new(timecode: first_paint_on_caption.timecode, grid: last_paint_on_caption.grid, mode: :pop_on) unless first_paint_on_caption.nil?
|
84
|
+
first_paint_on_caption = nil
|
85
|
+
last_paint_on_caption = nil
|
86
|
+
result_captions << caption unless caption.nil?
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
67
91
|
private
|
68
92
|
|
69
93
|
# Properties in order of priority (first element has the highest priority)
|
70
94
|
# The priority indicates in what order new style nodes should be created when their order
|
71
95
|
# would be indeterminate otherwise. This is required for getting deterministic output.
|
72
|
-
PROPERTIES = %i
|
96
|
+
PROPERTIES = %i[color underline italics flash].freeze
|
73
97
|
|
74
98
|
# Get the relative priority of a property
|
75
99
|
def property_priority(property)
|
@@ -140,7 +164,7 @@ module Subconv
|
|
140
164
|
default_style = CharacterStyle.default
|
141
165
|
# Start out with the default style
|
142
166
|
current_style = CharacterStyle.default
|
143
|
-
current_text = ''
|
167
|
+
current_text = +''
|
144
168
|
# Start with a stack of just the root node
|
145
169
|
parent_node_stack = [RootNode.new]
|
146
170
|
|
@@ -154,7 +178,7 @@ module Subconv
|
|
154
178
|
unless current_text.empty?
|
155
179
|
# Insert text node into the children of the node on top of the stack
|
156
180
|
parent_node_stack.last.children.push(TextNode.new(current_text))
|
157
|
-
current_text = ''
|
181
|
+
current_text = +''
|
158
182
|
end
|
159
183
|
|
160
184
|
# First close any nodes whose old value was different from the default value and has now changed
|
@@ -169,9 +193,10 @@ module Subconv
|
|
169
193
|
fail 'No node for property to close found in stack' if first_matching_node_index.nil?
|
170
194
|
|
171
195
|
# Collect styles below it that should _not_ be closed for possible re-opening because they would otherwise get lost
|
172
|
-
|
173
|
-
|
174
|
-
}
|
196
|
+
reopen_node_classes = parent_node_stack[first_matching_node_index..-1].reject { |node|
|
197
|
+
differences_to_close.any? { |difference| node.instance_of?(node_class_for_property(difference)) }
|
198
|
+
}
|
199
|
+
reopen = reopen_node_classes.map { |node| property_for_node_class(node.class) }
|
175
200
|
|
176
201
|
# Add them to the differences (since the current style changed from what was assumed above)
|
177
202
|
differences += reopen
|
@@ -189,6 +214,7 @@ module Subconv
|
|
189
214
|
value_now = column.style.send(property)
|
190
215
|
(column_index + 1...chunk.length).each do |check_column_index|
|
191
216
|
break if chunk[check_column_index].style.send(property) != value_now
|
217
|
+
|
192
218
|
length += 1
|
193
219
|
end
|
194
220
|
# Sort first by length, then by property priority
|
@@ -216,9 +242,7 @@ module Subconv
|
|
216
242
|
end
|
217
243
|
|
218
244
|
# Add any leftover text
|
219
|
-
unless current_text.empty?
|
220
|
-
parent_node_stack.last.children.push(TextNode.new(current_text))
|
221
|
-
end
|
245
|
+
parent_node_stack.last.children.push(TextNode.new(current_text)) unless current_text.empty?
|
222
246
|
# Return the root node
|
223
247
|
parent_node_stack.first
|
224
248
|
end
|
@@ -240,20 +264,27 @@ module Subconv
|
|
240
264
|
ColorNode.new(value.to_symbol)
|
241
265
|
else
|
242
266
|
fail 'Cannot create boolean property node for property off' unless value
|
267
|
+
|
243
268
|
property_class.new
|
244
269
|
end
|
245
270
|
end
|
246
271
|
|
247
272
|
# Determine all properties (as array of symbols) that are different between the
|
248
|
-
# Scc::CharacterStyle instances
|
249
|
-
def style_differences(
|
250
|
-
PROPERTIES.
|
251
|
-
value_a =
|
252
|
-
value_b =
|
273
|
+
# Scc::CharacterStyle instances style_a and style_b
|
274
|
+
def style_differences(style_a, style_b)
|
275
|
+
PROPERTIES.reject { |property|
|
276
|
+
value_a = style_a.send(property)
|
277
|
+
value_b = style_b.send(property)
|
253
278
|
|
254
|
-
value_a
|
279
|
+
value_a == value_b
|
255
280
|
}
|
256
281
|
end
|
282
|
+
|
283
|
+
# Shorthand for creating an empty grid if it is nil (in case the grid object is required).
|
284
|
+
# Grids can be nil on Scc::Caption instances when the grid would be empty
|
285
|
+
def require_grid_object(grid)
|
286
|
+
grid || Scc::Grid.new
|
287
|
+
end
|
257
288
|
end
|
258
289
|
end
|
259
290
|
end
|
data/lib/subconv/utility.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Subconv
|
2
4
|
module Utility
|
3
5
|
class InvalidTimespanError < RuntimeError; end
|
@@ -20,11 +22,12 @@ module Subconv
|
|
20
22
|
def self.clamp(value, min, max)
|
21
23
|
return min if value < min
|
22
24
|
return max if value > max
|
25
|
+
|
23
26
|
value
|
24
27
|
end
|
25
28
|
|
26
29
|
def self.node_to_tree_string(node, level = 0)
|
27
|
-
node_text = node.class.to_s
|
30
|
+
node_text = node.class.to_s.dup
|
28
31
|
if node.is_a?(TextNode)
|
29
32
|
node_text << " \"#{node.text}\""
|
30
33
|
elsif node.is_a?(ColorNode)
|
data/lib/subconv/version.rb
CHANGED
@@ -1,13 +1,14 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
|
2
3
|
require 'subconv/utility'
|
3
4
|
require 'subconv/caption'
|
4
5
|
|
5
6
|
module Subconv
|
6
7
|
module WebVtt
|
7
|
-
FILE_MAGIC = 'WEBVTT'
|
8
|
+
FILE_MAGIC = 'WEBVTT'
|
8
9
|
|
9
|
-
TIMECODE_FORMAT = '%02d:%02d:%02d.%03d'
|
10
|
-
CUE_FORMAT = '
|
10
|
+
TIMECODE_FORMAT = '%02d:%02d:%02d.%03d'
|
11
|
+
CUE_FORMAT = '%<start_time>s --> %<end_time>s %<settings>s'
|
11
12
|
|
12
13
|
# WebVTT caption writer
|
13
14
|
class Writer
|
@@ -27,37 +28,9 @@ module Subconv
|
|
27
28
|
|
28
29
|
# Write a single Scc::Caption to an IO stream
|
29
30
|
def write_caption(io, caption)
|
30
|
-
settings =
|
31
|
-
'align' => caption.align.to_s
|
32
|
-
}
|
33
|
-
if caption.position.is_a?(Position)
|
34
|
-
settings['line'] = webvtt_percentage(caption.position.y)
|
35
|
-
settings['position'] = webvtt_percentage(caption.position.x)
|
36
|
-
else
|
37
|
-
settings['line'] = case caption.position
|
38
|
-
when :top
|
39
|
-
# '0' would be better here, but Chrome does not support that yet
|
40
|
-
'5%'
|
41
|
-
when :bottom
|
42
|
-
'-1,end'
|
43
|
-
else
|
44
|
-
fail "Unknown position #{caption.position}"
|
45
|
-
end
|
46
|
-
end
|
31
|
+
settings = settings_to_string(caption_settings(caption))
|
47
32
|
|
48
|
-
|
49
|
-
settings.delete('align') if settings['align'] == 'middle'
|
50
|
-
|
51
|
-
# Convert settings to string representation
|
52
|
-
settings_string = settings.map { |setting|
|
53
|
-
setting.join(':')
|
54
|
-
}.join(' ')
|
55
|
-
|
56
|
-
io.write(CUE_FORMAT % {
|
57
|
-
start_time: timecode_to_webvtt(caption.timespan.start_time),
|
58
|
-
end_time: timecode_to_webvtt(caption.timespan.end_time),
|
59
|
-
settings: settings_string
|
60
|
-
} + "\n")
|
33
|
+
io.write(format(CUE_FORMAT, start_time: timecode_to_webvtt(caption.timespan.start_time), end_time: timecode_to_webvtt(caption.timespan.end_time), settings: settings) + "\n")
|
61
34
|
text = node_to_webvtt_markup caption.content
|
62
35
|
if @options[:trim_line_whitespace]
|
63
36
|
# Trim leading and trailing whitespace from each line
|
@@ -73,6 +46,44 @@ module Subconv
|
|
73
46
|
format('%.3f%%', (value * 100.0))
|
74
47
|
end
|
75
48
|
|
49
|
+
# Convert simple top/bottom positions to their corresponding WebVTT setting
|
50
|
+
def webvtt_simple_position(position)
|
51
|
+
case position
|
52
|
+
when :top
|
53
|
+
# '0' would be better here, but Chrome does not support that yet
|
54
|
+
'5%'
|
55
|
+
when :bottom
|
56
|
+
'-1,end'
|
57
|
+
else
|
58
|
+
fail ArgumentError, "Unknown position #{caption.position}"
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
# Convert WebVTT cue settings from a caption
|
63
|
+
def caption_settings(caption)
|
64
|
+
settings = {
|
65
|
+
'align' => caption.align.to_s
|
66
|
+
}
|
67
|
+
if caption.position.is_a?(Position)
|
68
|
+
settings['line'] = webvtt_percentage(caption.position.y)
|
69
|
+
settings['position'] = webvtt_percentage(caption.position.x)
|
70
|
+
else
|
71
|
+
settings['line'] = webvtt_simple_position(caption.position)
|
72
|
+
end
|
73
|
+
|
74
|
+
# Remove align if it is the default value anyway
|
75
|
+
settings.delete('align') if settings['align'] == 'middle'
|
76
|
+
|
77
|
+
settings
|
78
|
+
end
|
79
|
+
|
80
|
+
# Convert a setting hash to the WebVTT settings string
|
81
|
+
def settings_to_string(settings)
|
82
|
+
settings.map { |setting|
|
83
|
+
setting.join(':')
|
84
|
+
}.join(' ')
|
85
|
+
end
|
86
|
+
|
76
87
|
# Convert a timecode to the h/m/s format required by WebVTT
|
77
88
|
def timecode_to_webvtt(time)
|
78
89
|
value = time.to_seconds
|
data/spec/.rubocop.yml
ADDED
data/spec/caption_filter_spec.rb
CHANGED
data/spec/scc/reader_spec.rb
CHANGED
@@ -1,8 +1,11 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'spec_helper'
|
2
4
|
|
3
5
|
module Subconv
|
4
6
|
# Often used codes:
|
5
7
|
# 9420: Resume caption loading
|
8
|
+
# 9429: Resume direct captioning
|
6
9
|
# 91d0: Preamble address code for row 0 column 0
|
7
10
|
# 942f: End of caption
|
8
11
|
describe Scc::Reader do
|
@@ -10,6 +13,7 @@ module Subconv
|
|
10
13
|
|
11
14
|
let(:backspace) { '94a1 94a1' }
|
12
15
|
let(:test) { '54e5 73f4' }
|
16
|
+
let(:text) { '54e5 f8f4' }
|
13
17
|
let(:a) { 'c180' }
|
14
18
|
|
15
19
|
before(:each) do
|
@@ -42,15 +46,11 @@ module Subconv
|
|
42
46
|
end
|
43
47
|
|
44
48
|
it 'should decode simple text' do
|
45
|
-
|
46
|
-
|
47
|
-
expect(get_captions(caption_at_zero("9420 91d0 #{test} 942f"))).to eq([Scc::Caption.new(timecode: Timecode.new(4, default_fps), grid: expected_grid)])
|
49
|
+
expect(get_captions(caption_at_zero("9420 91d0 #{test} 942f"))).to eq(grids_to_captions(4 => 'Test'))
|
48
50
|
end
|
49
51
|
|
50
52
|
it 'should not reject data with wrong parity when checking is not requested' do
|
51
|
-
|
52
|
-
|
53
|
-
expect(get_captions(caption_at_zero("9420 11d0 #{test} 942f"), default_fps, false)).to eq([Scc::Caption.new(timecode: Timecode.new(4, default_fps), grid: expected_grid)])
|
53
|
+
expect(get_captions(caption_at_zero("9420 11d0 #{test} 942f"), default_fps, false)).to eq(grids_to_captions(4 => 'Test'))
|
54
54
|
end
|
55
55
|
|
56
56
|
expected_rows = {
|
@@ -73,8 +73,7 @@ module Subconv
|
|
73
73
|
|
74
74
|
expected_rows.each_pair do |pac, row|
|
75
75
|
it "should decode the preamble address code #{pac} to row #{row}" do
|
76
|
-
|
77
|
-
expect(get_captions(caption_at_zero("9420 #{pac} #{test} 942f"))).to eq([Scc::Caption.new(timecode: Timecode.new(4, default_fps), grid: expected_grid)])
|
76
|
+
expect(get_captions(caption_at_zero("9420 #{pac} #{test} 942f"))).to eq(grids_to_captions(4 => Scc::Grid.new.insert_text(row, 0, 'Test')))
|
78
77
|
end
|
79
78
|
end
|
80
79
|
|
@@ -91,31 +90,31 @@ module Subconv
|
|
91
90
|
|
92
91
|
expected_column.each_pair do |pac, column|
|
93
92
|
it "should decode the preamble address code #{pac} to column #{column}" do
|
94
|
-
|
95
|
-
expect(get_captions(caption_at_zero("9420 #{pac} #{test} 942f"))).to eq([Scc::Caption.new(timecode: Timecode.new(4, default_fps), grid: expected_grid)])
|
93
|
+
expect(get_captions(caption_at_zero("9420 #{pac} #{test} 942f"))).to eq(grids_to_captions(4 => Scc::Grid.new.insert_text(0, column, 'Test')))
|
96
94
|
end
|
97
95
|
end
|
98
96
|
|
97
|
+
it 'should reset column on style preamble address code' do
|
98
|
+
expect(get_captions(caption_at_zero("9420 9152 #{text} 9140 #{test} 942f"))).to eq(grids_to_captions(7 => 'TestText'))
|
99
|
+
end
|
100
|
+
|
99
101
|
it 'should handle backspace' do
|
100
|
-
expected_grid = Scc::Grid.new.insert_text(1, 0, 'Test')
|
101
102
|
# Write AA, then backspace three times, go to next row, write Tesu, backspace one time, then write t
|
102
103
|
# -> Should result in the first line being empty and the second line reading "Test"
|
103
|
-
expect(get_captions(caption_at_zero('9420 91d0 c1c1 ' + (backspace + ' ') * 3 + "91e0 54e5 7375 #{backspace} f480 942f"))).to eq(
|
104
|
+
expect(get_captions(caption_at_zero('9420 91d0 c1c1 ' + (backspace + ' ') * 3 + "91e0 54e5 7375 #{backspace} f480 942f"))).to eq(grids_to_captions(15 => Scc::Grid.new.insert_text(1, 0, 'Test')))
|
104
105
|
end
|
105
106
|
|
106
107
|
it 'should handle overflowing lines' do
|
107
|
-
|
108
|
-
# Write 40 As and then tw Bs
|
108
|
+
# Write 40 As and then two Bs
|
109
109
|
# -> Should result in 31 As and one B since after overflow the last column is overwritten the whole time
|
110
|
-
expect(get_captions(caption_at_zero('9420 91d0 ' + ('c1c1 ' * 20) + 'c2c2 942f'))).to eq(
|
110
|
+
expect(get_captions(caption_at_zero('9420 91d0 ' + ('c1c1 ' * 20) + 'c2c2 942f'))).to eq(grids_to_captions(23 => 'A' * 31 + 'B'))
|
111
111
|
end
|
112
112
|
|
113
113
|
it 'should handle italics' do
|
114
114
|
style = Scc::CharacterStyle.default
|
115
115
|
style.italics = true
|
116
116
|
# Italics is a mid-row spacing code, so expect a space before the text
|
117
|
-
|
118
|
-
expect(get_captions(caption_at_zero("9420 91d0 91ae #{test} 942f"))).to eq([Scc::Caption.new(timecode: Timecode.new(5, default_fps), grid: expected_grid)])
|
117
|
+
expect(get_captions(caption_at_zero("9420 91d0 91ae #{test} 942f"))).to eq(grids_to_captions(5 => Scc::Grid.new.insert_text(0, 0, ' ').insert_text(0, 1, 'Test', style)))
|
119
118
|
end
|
120
119
|
|
121
120
|
it 'should turn off flash on italics mid-row codes' do
|
@@ -124,32 +123,28 @@ module Subconv
|
|
124
123
|
style.italics = true
|
125
124
|
flash_style.flash = true
|
126
125
|
# Two spaces before the text
|
127
|
-
expected_grid = Scc::Grid.new.insert_text(0, 0, ' ').insert_text(0, 1, ' ', flash_style).insert_text(0, 2, 'Test', style)
|
128
126
|
# "<flash on><italics>Test"
|
129
|
-
expect(get_captions(caption_at_zero("9420 91d0 94a8 91ae #{test} 942f"))).to eq(
|
127
|
+
expect(get_captions(caption_at_zero("9420 91d0 94a8 91ae #{test} 942f"))).to eq(grids_to_captions(6 => Scc::Grid.new.insert_text(0, 0, ' ').insert_text(0, 1, ' ', flash_style).insert_text(0, 2, 'Test', style)))
|
130
128
|
end
|
131
129
|
|
132
130
|
it 'should handle italics preamble address code' do
|
133
131
|
style = Scc::CharacterStyle.default
|
134
132
|
style.italics = true
|
135
|
-
|
136
|
-
expect(get_captions(caption_at_zero("9420 91ce #{test} 942f"))).to eq([Scc::Caption.new(timecode: Timecode.new(4, default_fps), grid: expected_grid)])
|
133
|
+
expect(get_captions(caption_at_zero("9420 91ce #{test} 942f"))).to eq(grids_to_captions(4 => Scc::Grid.new.insert_text(0, 0, 'Test', style)))
|
137
134
|
end
|
138
135
|
|
139
136
|
it 'should handle italics and underline preamble address code' do
|
140
137
|
style = Scc::CharacterStyle.default
|
141
138
|
style.italics = true
|
142
139
|
style.underline = true
|
143
|
-
|
144
|
-
expect(get_captions(caption_at_zero("9420 914f #{test} 942f"))).to eq([Scc::Caption.new(timecode: Timecode.new(4, default_fps), grid: expected_grid)])
|
140
|
+
expect(get_captions(caption_at_zero("9420 914f #{test} 942f"))).to eq(grids_to_captions(4 => Scc::Grid.new.insert_text(0, 0, 'Test', style)))
|
145
141
|
end
|
146
142
|
|
147
143
|
it 'should handle underline' do
|
148
144
|
style = Scc::CharacterStyle.default
|
149
145
|
style.underline = true
|
150
146
|
# Underline is a mid-row spacing code, so expect a space before the text
|
151
|
-
|
152
|
-
expect(get_captions(caption_at_zero("9420 91d0 91a1 #{test} 942f"))).to eq([Scc::Caption.new(timecode: Timecode.new(5, default_fps), grid: expected_grid)])
|
147
|
+
expect(get_captions(caption_at_zero("9420 91d0 91a1 #{test} 942f"))).to eq(grids_to_captions(5 => Scc::Grid.new.insert_text(0, 0, ' ').insert_text(0, 1, 'Test', style)))
|
153
148
|
end
|
154
149
|
|
155
150
|
it 'should handle italics and underline' do
|
@@ -157,8 +152,7 @@ module Subconv
|
|
157
152
|
style.italics = true
|
158
153
|
style.underline = true
|
159
154
|
# Italics/underline is a mid-row spacing code, so expect a space before the text
|
160
|
-
|
161
|
-
expect(get_captions(caption_at_zero("9420 91d0 912f #{test} 942f"))).to eq([Scc::Caption.new(timecode: Timecode.new(5, default_fps), grid: expected_grid)])
|
155
|
+
expect(get_captions(caption_at_zero("9420 91d0 912f #{test} 942f"))).to eq(grids_to_captions(5 => Scc::Grid.new.insert_text(0, 0, ' ').insert_text(0, 1, 'Test', style)))
|
162
156
|
end
|
163
157
|
|
164
158
|
color_code_map = {
|
@@ -175,8 +169,7 @@ module Subconv
|
|
175
169
|
it "should handle the color #{color}" do
|
176
170
|
style = Scc::CharacterStyle.default
|
177
171
|
style.color = color
|
178
|
-
|
179
|
-
expect(get_captions(caption_at_zero("9420 91d0 #{color_code} #{backspace} #{a} 942f"))).to eq([Scc::Caption.new(timecode: Timecode.new(6, default_fps), grid: expected_grid)])
|
172
|
+
expect(get_captions(caption_at_zero("9420 91d0 #{color_code} #{backspace} #{a} 942f"))).to eq(grids_to_captions(6 => Scc::Grid.new.insert_text(0, 0, 'A', style)))
|
180
173
|
end
|
181
174
|
end
|
182
175
|
|
@@ -186,9 +179,8 @@ module Subconv
|
|
186
179
|
style.color = Scc::Color::RED
|
187
180
|
italics_style.italics = true
|
188
181
|
# Two spaces before the text
|
189
|
-
expected_grid = Scc::Grid.new.insert_text(0, 0, ' ').insert_text(0, 1, ' ', italics_style).insert_text(0, 2, 'Test', style)
|
190
182
|
# "<italics><red>Test"
|
191
|
-
expect(get_captions(caption_at_zero("9420 91d0 91ae 91a8 #{test} 942f"))).to eq(
|
183
|
+
expect(get_captions(caption_at_zero("9420 91d0 91ae 91a8 #{test} 942f"))).to eq(grids_to_captions(6 => Scc::Grid.new.insert_text(0, 0, ' ').insert_text(0, 1, ' ', italics_style).insert_text(0, 2, 'Test', style)))
|
192
184
|
end
|
193
185
|
|
194
186
|
it 'should handle all available attributes combined' do
|
@@ -203,109 +195,192 @@ module Subconv
|
|
203
195
|
style.flash = true
|
204
196
|
expected_grid.insert_text(0, 2, 'Test', style.dup)
|
205
197
|
# Set color via preamble address code to test that too, then set italics/underline via mid-row code, then assign flash via "flash on" miscellaneous control code
|
206
|
-
expect(get_captions(caption_at_zero("9420 91c8 912f 94a8 #{test} 942f"))).to eq(
|
198
|
+
expect(get_captions(caption_at_zero("9420 91c8 912f 94a8 #{test} 942f"))).to eq(grids_to_captions(6 => expected_grid))
|
207
199
|
end
|
208
200
|
|
209
201
|
it 'should handle transparent space' do
|
210
202
|
# Column 1 should be empty
|
211
|
-
expected_grid = Scc::Grid.new.insert_text(0, 0, 'A').insert_text(0, 2, 'A')
|
212
203
|
# "A<transparent space>A"
|
213
|
-
expect(get_captions(caption_at_zero("9420 91d0 #{a} 91b9 #{a} 942f"))).to eq(
|
204
|
+
expect(get_captions(caption_at_zero("9420 91d0 #{a} 91b9 #{a} 942f"))).to eq(grids_to_captions(5 => Scc::Grid.new.insert_text(0, 0, 'A').insert_text(0, 2, 'A')))
|
214
205
|
end
|
215
206
|
|
216
207
|
it 'should delete characters behind transparent space' do
|
217
208
|
# Column 1 should be empty
|
218
|
-
expected_grid = Scc::Grid.new.insert_text(0, 1, 'est')
|
219
209
|
# "Test<PAC><transparent space>"
|
220
|
-
expect(get_captions(caption_at_zero("9420 91d0 #{test} 91d0 91b9 942f"))).to eq(
|
210
|
+
expect(get_captions(caption_at_zero("9420 91d0 #{test} 91d0 91b9 942f"))).to eq(grids_to_captions(6 => Scc::Grid.new.insert_text(0, 1, 'est')))
|
221
211
|
end
|
222
212
|
|
223
213
|
it 'should handle standard space' do
|
224
|
-
expected_grid = Scc::Grid.new.insert_text(0, 0, 'A A')
|
225
214
|
# "A A"
|
226
|
-
expect(get_captions(caption_at_zero('9420 91d0 c120 c180 942f'))).to eq(
|
215
|
+
expect(get_captions(caption_at_zero('9420 91d0 c120 c180 942f'))).to eq(grids_to_captions(4 => 'A A'))
|
227
216
|
end
|
228
217
|
|
229
218
|
it 'should handle tab offset' do
|
230
219
|
# Space between the As should be empty
|
231
220
|
expected_grid = Scc::Grid.new.insert_text(0, 0, 'A').insert_text(0, 2, 'A').insert_text(0, 5, 'A').insert_text(0, 9, 'A')
|
232
221
|
# "A<tab offset 1>A<tab offset 2>A<tab offset 3>A"
|
233
|
-
expect(get_captions(caption_at_zero("9420 91d0 #{a} 97a1 #{a} 97a2 #{a} 9723 #{a} 942f"))).to eq(
|
222
|
+
expect(get_captions(caption_at_zero("9420 91d0 #{a} 97a1 #{a} 97a2 #{a} 9723 #{a} 942f"))).to eq(grids_to_captions(9 => expected_grid))
|
234
223
|
end
|
235
224
|
|
236
225
|
it 'should not delete characters on tab offset' do
|
237
226
|
# Space between the As should be empty
|
238
|
-
expected_grid = Scc::Grid.new.insert_text(0, 0, 'TAst')
|
239
227
|
# "Test<PAC><tab offset 1>A"
|
240
|
-
expect(get_captions(caption_at_zero("9420 91d0 #{test} 91d0 97a1 #{a} 942f"))).to eq(
|
228
|
+
expect(get_captions(caption_at_zero("9420 91d0 #{test} 91d0 97a1 #{a} 942f"))).to eq(grids_to_captions(7 => 'TAst'))
|
241
229
|
end
|
242
230
|
|
243
231
|
it 'should ignore repeated commands' do
|
244
232
|
# There should be only one, not two spaces between the As
|
245
|
-
expected_grid = Scc::Grid.new.insert_text(0, 0, 'A').insert_text(0, 2, 'A')
|
246
233
|
# "A<transparent space><transparent space>A"
|
247
|
-
expect(get_captions(caption_at_zero("9420 91d0 #{a} 91b9 91b9 #{a} 942f"))).to eq(
|
234
|
+
expect(get_captions(caption_at_zero("9420 91d0 #{a} 91b9 91b9 #{a} 942f"))).to eq(grids_to_captions(6 => Scc::Grid.new.insert_text(0, 0, 'A').insert_text(0, 2, 'A')))
|
248
235
|
end
|
249
236
|
|
250
237
|
it 'should not ignore multiply repeated commands' do
|
251
238
|
# Now there should be not, not four or one, spaces between the As
|
252
|
-
expected_grid = Scc::Grid.new.insert_text(0, 0, 'A').insert_text(0, 3, 'A')
|
253
239
|
# "A<transparent space><transparent space<transparent space><transparent space>>A"
|
254
|
-
expect(get_captions(caption_at_zero("9420 91d0 #{a} 91b9 91b9 91b9 91b9 #{a} 942f"))).to eq(
|
240
|
+
expect(get_captions(caption_at_zero("9420 91d0 #{a} 91b9 91b9 91b9 91b9 #{a} 942f"))).to eq(grids_to_captions(8 => Scc::Grid.new.insert_text(0, 0, 'A').insert_text(0, 3, 'A')))
|
255
241
|
end
|
256
242
|
|
257
243
|
it 'should handle delete to end of row' do
|
258
|
-
expected_grid = Scc::Grid.new.insert_text(0, 0, 'AAAA')
|
259
244
|
# "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA<PAC indent 4><delete to end of row>"
|
260
|
-
expect(get_captions(caption_at_zero('9420 91d0 ' + (a + ' ') * 32 + '9152 94a4 942f'))).to eq(
|
245
|
+
expect(get_captions(caption_at_zero('9420 91d0 ' + (a + ' ') * 32 + '9152 94a4 942f'))).to eq(grids_to_captions(36 => 'AAAA'))
|
261
246
|
end
|
262
247
|
|
263
248
|
it 'should handle erase displayed memory' do
|
264
|
-
expected_grid = Scc::Grid.new.insert_text(0, 0, 'Test')
|
265
249
|
# Display "Test", wait 2 frames, erase displayed memory
|
266
250
|
# -> should result in an empty caption 3 frames later
|
267
|
-
expect(get_captions(caption_at_zero("9420 91d0 #{test} 942f 8080 8080 942c"))).to eq(
|
251
|
+
expect(get_captions(caption_at_zero("9420 91d0 #{test} 942f 8080 8080 942c"))).to eq(grids_to_captions(4 => 'Test', 7 => nil))
|
268
252
|
end
|
269
253
|
|
270
254
|
it 'should handle erase non-displayed memory' do
|
271
|
-
expected_grid = Scc::Grid.new.insert_text(0, 0, 'A')
|
272
255
|
# Insert "Test", delete the non-displayed memory, insert "A" and then flip captions
|
273
|
-
expect(get_captions(caption_at_zero("9420 91d0 #{test} 94ae 91d0 #{a} 942f"))).to eq(
|
256
|
+
expect(get_captions(caption_at_zero("9420 91d0 #{test} 94ae 91d0 #{a} 942f"))).to eq(grids_to_captions(7 => 'A'))
|
274
257
|
end
|
275
258
|
|
276
259
|
it 'should handle special characters' do
|
277
|
-
|
278
|
-
|
260
|
+
expect(get_captions(caption_at_zero('9420 91d0 9137 942f'))).to eq(grids_to_captions(3 => '♪'))
|
261
|
+
end
|
262
|
+
|
263
|
+
it 'should handle extended characters' do
|
264
|
+
expect(get_captions(caption_at_zero("9420 91d0 #{a} 92a4 #{a} 133b 942f"))).to eq(grids_to_captions(6 => 'Üø'))
|
279
265
|
end
|
280
266
|
|
281
267
|
it 'should handle multiple timecodes and captions' do
|
282
268
|
expected_grid_test = Scc::Grid.new.insert_text(0, 0, 'Test')
|
283
269
|
expected_grid_a = Scc::Grid.new.insert_text(0, 0, 'A')
|
284
270
|
expected_grid_b = Scc::Grid.new.insert_text(0, 0, 'B')
|
285
|
-
caption_text =
|
286
|
-
Scenarist_SCC V1.0
|
271
|
+
caption_text = <<~"SCC"
|
272
|
+
Scenarist_SCC V1.0
|
287
273
|
|
288
|
-
00:00:01:00\t9420 91d0 #{test} 942f 94ae
|
274
|
+
00:00:01:00\t9420 91d0 #{test} 942f 94ae
|
289
275
|
|
290
|
-
00:00:02:00\t9420 91d0 #{test} 942f 94ae
|
276
|
+
00:00:02:00\t9420 91d0 #{test} 942f 94ae
|
291
277
|
|
292
|
-
00:00:03:00\t942c
|
278
|
+
00:00:03:00\t942c
|
293
279
|
|
294
|
-
00:00:04:00\t9420 91d0 c180 942f
|
280
|
+
00:00:04:00\t9420 91d0 c180 942f
|
295
281
|
|
296
|
-
00:00:05:00\t9420 91d0 c280 942f
|
297
|
-
|
282
|
+
00:00:05:00\t9420 91d0 c280 942f
|
283
|
+
SCC
|
298
284
|
expect(get_captions(caption_text)).to eq([
|
299
285
|
# First caption: Test
|
300
|
-
Scc::Caption.new(timecode: Timecode.parse('00:00:01:04', default_fps), grid: expected_grid_test),
|
286
|
+
Scc::Caption.new(timecode: Timecode.parse('00:00:01:04', default_fps), grid: expected_grid_test, mode: :pop_on),
|
301
287
|
# Caption at 00:00:02:00 should be identical to the first one and thus not get put out
|
302
288
|
# At 00:00:03:00: erase displayed caption
|
303
|
-
Scc::Caption.new(timecode: Timecode.parse('00:00:03:00', default_fps)),
|
289
|
+
Scc::Caption.new(timecode: Timecode.parse('00:00:03:00', default_fps), mode: :pop_on),
|
304
290
|
# At 00:00:04:00: Display "A"
|
305
|
-
Scc::Caption.new(timecode: Timecode.parse('00:00:04:03', default_fps), grid: expected_grid_a),
|
291
|
+
Scc::Caption.new(timecode: Timecode.parse('00:00:04:03', default_fps), grid: expected_grid_a, mode: :pop_on),
|
306
292
|
# At 00:00:05:00: Display "B" without erasing A beforehand
|
307
|
-
Scc::Caption.new(timecode: Timecode.parse('00:00:05:03', default_fps), grid: expected_grid_b)
|
293
|
+
Scc::Caption.new(timecode: Timecode.parse('00:00:05:03', default_fps), grid: expected_grid_b, mode: :pop_on)
|
308
294
|
])
|
309
295
|
end
|
296
|
+
|
297
|
+
context 'in paint-on mode' do
|
298
|
+
it 'should produce one caption for every change' do
|
299
|
+
expected_grids = %w[Te Test TestTe TestText TestTex].map { |text| Scc::Grid.new.insert_text(0, 0, text) }
|
300
|
+
expect(get_captions(caption_at_zero("9429 91d0 #{test} #{text} #{backspace}"))).to eq(
|
301
|
+
expected_grids.each_with_index.map { |grid, i|
|
302
|
+
Scc::Caption.new(timecode: Timecode.new(i + 2, default_fps), grid: grid, mode: :paint_on)
|
303
|
+
}
|
304
|
+
)
|
305
|
+
end
|
306
|
+
|
307
|
+
it 'should handle special characters' do
|
308
|
+
expect(get_captions(caption_at_zero('9429 91d0 9137'))).to eq(grids_to_captions(:paint_on, 2 => '♪'))
|
309
|
+
end
|
310
|
+
|
311
|
+
it 'should handle style attributes' do
|
312
|
+
style = Scc::CharacterStyle.default
|
313
|
+
style.italics = true
|
314
|
+
# Italics is a mid-row spacing code, so expect a space before the text
|
315
|
+
expected_grids = {
|
316
|
+
2 => Scc::Grid.new.insert_text(0, 0, ' '),
|
317
|
+
3 => Scc::Grid.new.insert_text(0, 0, ' ').insert_text(0, 1, 'Te', style),
|
318
|
+
4 => Scc::Grid.new.insert_text(0, 0, ' ').insert_text(0, 1, 'Test', style)
|
319
|
+
}
|
320
|
+
expect(get_captions(caption_at_zero("9429 91d0 91ae #{test}"))).to eq(grids_to_captions(:paint_on, expected_grids))
|
321
|
+
end
|
322
|
+
|
323
|
+
it 'should handle backspace' do
|
324
|
+
expected_grids = {
|
325
|
+
2 => 'Te',
|
326
|
+
3 => 'Test',
|
327
|
+
4 => 'Tes',
|
328
|
+
6 => 'TesTe',
|
329
|
+
7 => 'TesText',
|
330
|
+
8 => 'TesTex',
|
331
|
+
10 => 'TesTe'
|
332
|
+
}
|
333
|
+
expect(get_captions(caption_at_zero("9429 91d0 #{test} #{backspace} #{text} #{backspace} #{backspace}"))).to eq(grids_to_captions(:paint_on, expected_grids))
|
334
|
+
end
|
335
|
+
|
336
|
+
it 'should handle erase displayed memory' do
|
337
|
+
expect(get_captions(caption_at_zero("9429 91d0 #{test} 8080 942c"))).to eq(grids_to_captions(:paint_on, 2 => 'Te', 3 => 'Test', 5 => nil))
|
338
|
+
end
|
339
|
+
|
340
|
+
it 'should not change captions on erase non-displayed memory' do
|
341
|
+
expect(get_captions(caption_at_zero("9429 91d0 #{test} 94ae"))).to eq(grids_to_captions(:paint_on, 2 => 'Te', 3 => 'Test'))
|
342
|
+
end
|
343
|
+
|
344
|
+
it 'should handle delete to end of row' do
|
345
|
+
expected_grids = {
|
346
|
+
2 => 'A',
|
347
|
+
3 => 'AA',
|
348
|
+
4 => 'AAA',
|
349
|
+
5 => 'AAAA',
|
350
|
+
6 => 'AAAATe',
|
351
|
+
7 => 'AAAATest',
|
352
|
+
9 => 'AAAA'
|
353
|
+
}
|
354
|
+
# "AAAATest<PAC indent 4><delete to end of row>"
|
355
|
+
expect(get_captions(caption_at_zero('9429 91d0 ' + (a + ' ') * 4 + "#{test} 9152 94a4"))).to eq(grids_to_captions(:paint_on, expected_grids))
|
356
|
+
end
|
357
|
+
end
|
358
|
+
|
359
|
+
it 'should keep paint-on captions active when switching to pop-on mode' do
|
360
|
+
expected_captions = [
|
361
|
+
Scc::Caption.new(timecode: Timecode.new(2, default_fps), grid: Scc::Grid.new.insert_text(0, 0, 'A'), mode: :paint_on),
|
362
|
+
Scc::Caption.new(timecode: Timecode.new(6, default_fps), grid: Scc::Grid.new.insert_text(0, 1, 'Test'), mode: :pop_on)
|
363
|
+
]
|
364
|
+
expect(get_captions(caption_at_zero("9429 91d0 #{a} 9420 #{test} 942f"))).to eq(expected_captions)
|
365
|
+
end
|
366
|
+
|
367
|
+
it 'should keep paint-on captions in non-displayed memory when ending paint-on mode via end of caption' do
|
368
|
+
expected_captions = [
|
369
|
+
Scc::Caption.new(timecode: Timecode.new(2, default_fps), grid: Scc::Grid.new.insert_text(0, 0, 'Te'), mode: :paint_on),
|
370
|
+
Scc::Caption.new(timecode: Timecode.new(3, default_fps), grid: Scc::Grid.new.insert_text(0, 0, 'Test'), mode: :paint_on),
|
371
|
+
Scc::Caption.new(timecode: Timecode.new(4, default_fps), mode: :pop_on),
|
372
|
+
Scc::Caption.new(timecode: Timecode.new(7, default_fps), grid: Scc::Grid.new.insert_text(0, 0, 'TestText'), mode: :pop_on)
|
373
|
+
]
|
374
|
+
expect(get_captions(caption_at_zero("9429 91d0 #{test} 942f #{text} 942f"))).to eq(expected_captions)
|
375
|
+
end
|
376
|
+
|
377
|
+
it 'should keep displayed pop-on captions active when switching to paint-on mode' do
|
378
|
+
expected_captions = [
|
379
|
+
Scc::Caption.new(timecode: Timecode.new(3, default_fps), grid: Scc::Grid.new.insert_text(0, 0, 'Test'), mode: :pop_on),
|
380
|
+
Scc::Caption.new(timecode: Timecode.new(5, default_fps), grid: Scc::Grid.new.insert_text(0, 0, 'TestTe'), mode: :paint_on),
|
381
|
+
Scc::Caption.new(timecode: Timecode.new(6, default_fps), grid: Scc::Grid.new.insert_text(0, 0, 'TestText'), mode: :paint_on)
|
382
|
+
]
|
383
|
+
expect(get_captions(caption_at_zero("9420 #{test} 942f 9429 #{text}"))).to eq(expected_captions)
|
384
|
+
end
|
310
385
|
end
|
311
386
|
end
|