subconv 0.1.0 → 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.
- 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
|