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.
@@ -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