subconv 0.1.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
- unless caption.grid.nil?
33
- # Collect text chunks in each row and create captions out of them
34
- caption.grid.each_with_index do |row, row_number|
35
- chunks = collect_chunks(row)
36
- chunks.each_pair do |start_column, chunk|
37
- content = transform_chunk(chunk)
38
- position = position_from_grid(row_number, start_column)
39
- captions_open.push(Subconv::Caption.new(
40
- align: :start,
41
- position: position,
42
- content: content
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(color underline italics flash).freeze
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
- reopen = parent_node_stack[first_matching_node_index..-1].select { |node|
173
- !differences_to_close.any? { |difference| node.instance_of?(node_class_for_property(difference)) }
174
- }.map { |node| property_for_node_class(node.class) }
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 a and b
249
- def style_differences(a, b)
250
- PROPERTIES.select { |property|
251
- value_a = a.send(property)
252
- value_b = b.send(property)
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 != value_b
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
@@ -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)
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Subconv
2
- VERSION = '0.1.0'.freeze
4
+ VERSION = '1.0.0'
3
5
  end
@@ -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'.freeze
8
+ FILE_MAGIC = 'WEBVTT'
8
9
 
9
- TIMECODE_FORMAT = '%02d:%02d:%02d.%03d'.freeze
10
- CUE_FORMAT = '%{start_time} --> %{end_time} %{settings}'.freeze
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
- # Remove align if it is the default value anyway
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
@@ -0,0 +1,8 @@
1
+ inherit_from:
2
+ - ../.rubocop.yml
3
+
4
+ Metrics/ModuleLength:
5
+ Enabled: false
6
+
7
+ Metrics/BlockLength:
8
+ Enabled: false
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'spec_helper'
2
4
 
3
5
  module Subconv
@@ -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
- expected_grid = Scc::Grid.new.insert_text(0, 0, 'Test')
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
- expected_grid = Scc::Grid.new.insert_text(0, 0, 'Test')
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
- expected_grid = Scc::Grid.new.insert_text(row, 0, 'Test')
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
- expected_grid = Scc::Grid.new.insert_text(0, column, 'Test')
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([Scc::Caption.new(timecode: Timecode.new(15, default_fps), grid: expected_grid)])
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
- expected_grid = Scc::Grid.new.insert_text(0, 0, 'A' * 31 + 'B')
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([Scc::Caption.new(timecode: Timecode.new(23, default_fps), grid: expected_grid)])
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
- expected_grid = Scc::Grid.new.insert_text(0, 0, ' ').insert_text(0, 1, 'Test', style)
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([Scc::Caption.new(timecode: Timecode.new(6, default_fps), grid: expected_grid)])
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
- expected_grid = Scc::Grid.new.insert_text(0, 0, 'Test', style)
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
- expected_grid = Scc::Grid.new.insert_text(0, 0, 'Test', style)
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
- expected_grid = Scc::Grid.new.insert_text(0, 0, ' ').insert_text(0, 1, 'Test', style)
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
- expected_grid = Scc::Grid.new.insert_text(0, 0, ' ').insert_text(0, 1, 'Test', style)
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
- expected_grid = Scc::Grid.new.insert_text(0, 0, 'A', style)
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([Scc::Caption.new(timecode: Timecode.new(6, default_fps), grid: expected_grid)])
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([Scc::Caption.new(timecode: Timecode.new(6, default_fps), grid: expected_grid)])
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([Scc::Caption.new(timecode: Timecode.new(5, default_fps), grid: expected_grid)])
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([Scc::Caption.new(timecode: Timecode.new(6, default_fps), grid: expected_grid)])
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([Scc::Caption.new(timecode: Timecode.new(4, default_fps), grid: expected_grid)])
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([Scc::Caption.new(timecode: Timecode.new(9, default_fps), grid: expected_grid)])
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([Scc::Caption.new(timecode: Timecode.new(7, default_fps), grid: expected_grid)])
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([Scc::Caption.new(timecode: Timecode.new(6, default_fps), grid: expected_grid)])
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([Scc::Caption.new(timecode: Timecode.new(8, default_fps), grid: expected_grid)])
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([Scc::Caption.new(timecode: Timecode.new(36, default_fps), grid: expected_grid)])
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([Scc::Caption.new(timecode: Timecode.new(4, default_fps), grid: expected_grid), Scc::Caption.new(timecode: Timecode.new(7, default_fps))])
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([Scc::Caption.new(timecode: Timecode.new(7, default_fps), grid: expected_grid)])
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
- expected_grid = Scc::Grid.new.insert_text(0, 0, '♪')
278
- expect(get_captions(caption_at_zero('9420 91d0 9137 942f'))).to eq([Scc::Caption.new(timecode: Timecode.new(3, default_fps), grid: expected_grid)])
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 = <<"END"
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
- END
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