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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 28cd6dd4e5675ee424b07b6ad013fb694e9145b8
4
- data.tar.gz: b81878db9e8705adf103bd0ecd40466e54512f77
2
+ SHA256:
3
+ metadata.gz: 6348b3b43fd4b191b25d166007dd539d18d3e72c13f32cb23e7feb839c06d665
4
+ data.tar.gz: 8a0b8177d72cf42ff45649e28d27c333ad223072193181bcb023a6349c8e6ee5
5
5
  SHA512:
6
- metadata.gz: a7ac077fc7d2691d74141d49e99051b0679202ac40625f8150846c0f625db85614e636a866db1916b465290143dae41549dd4b98f4712bffd4e11e2c6a7b4995
7
- data.tar.gz: 646035c2eaaa3e9726621f552fdd44c0b27b56733f6a7dd70c4677e21fd72893e2748177727e027a6ab5719d9a8afc5ca0da0926f9d278922ed501431e034080
6
+ metadata.gz: d805a1a29e705a9fbc23ec481484600f99ef844fc51422469a1768d255d2894e8c7c06fe9c45079cf97b99a86b5bbb34df0c7c09083ac5cd493d084ebce605a7
7
+ data.tar.gz: bfbe89e2915ffc77484521d8e291cb8f58685dc8e5bf819c34c62bb8344b03c187dfcedce55363003ab1565d7eb59694f4cee1aee85c2a9e9b9cee84cb1d6ac6
data/.gitignore CHANGED
@@ -30,7 +30,7 @@ build/
30
30
 
31
31
  # for a library or gem, you might want to ignore these files since the code is
32
32
  # intended to run in multiple environments; otherwise, check them in:
33
- Gemfile.lock
33
+ #Gemfile.lock
34
34
 
35
35
  # .ruby-version
36
36
  # .ruby-gemset
data/.gitlab-ci.yml ADDED
@@ -0,0 +1,33 @@
1
+ before_script:
2
+ - apt-get update -qq
3
+ - ruby -v
4
+ - which ruby
5
+ - gem install bundler --no-document --version "~>1.0"
6
+ - bundle install --jobs $(nproc) "${FLAGS[@]}"
7
+
8
+ rspec:
9
+ script:
10
+ - bundle exec rake spec
11
+
12
+ rubocop:
13
+ script:
14
+ - bundle exec rake rubocop
15
+
16
+ dependency_scanning:
17
+ before_script: []
18
+ image: docker:stable
19
+ variables:
20
+ DOCKER_DRIVER: overlay2
21
+ allow_failure: true
22
+ services:
23
+ - docker:stable-dind
24
+ script:
25
+ - export SP_VERSION=$(echo "$CI_SERVER_VERSION" | sed 's/^\([0-9]*\)\.\([0-9]*\).*/\1-\2-stable/')
26
+ - docker run
27
+ --env DEP_SCAN_DISABLE_REMOTE_CHECKS="${DEP_SCAN_DISABLE_REMOTE_CHECKS:-false}"
28
+ --volume "$PWD:/code"
29
+ --volume /var/run/docker.sock:/var/run/docker.sock
30
+ "registry.gitlab.com/gitlab-org/security-products/dependency-scanning:$SP_VERSION" /code
31
+ artifacts:
32
+ reports:
33
+ dependency_scanning: gl-dependency-scanning-report.json
data/.rubocop.yml CHANGED
@@ -1,20 +1,14 @@
1
1
  AllCops:
2
- TargetRubyVersion: 2.2
2
+ TargetRubyVersion: 2.4
3
3
  Exclude:
4
4
  - '**/bin/**/*'
5
- - '**/db/schema.rb'
6
5
  - '**/config/**/*'
7
6
  - '**/lib/tasks/*.rake'
7
+ - 'vendor/**/*'
8
8
 
9
- # Use only ascii symbols in identifiers.
10
- AsciiComments:
11
- Enabled: false
12
-
13
- # Document classes and non-namespace modules.
14
9
  Documentation:
15
10
  Enabled: false
16
11
 
17
- # Limit lines to 79 characters.
18
12
  LineLength:
19
13
  Enabled: false
20
14
 
@@ -24,17 +18,17 @@ BlockDelimiters:
24
18
  MethodLength:
25
19
  Max: 30
26
20
 
27
- # Checks for proper usage of fail and raise.
28
- #SignalException:
29
- # Enabled: false
21
+ UncommunicativeMethodParamName:
22
+ AllowedNames:
23
+ - hi
24
+ - io
25
+ - lo
26
+ - x
27
+ - y
30
28
 
31
29
  StringLiterals:
32
30
  EnforcedStyle: single_quotes
33
31
 
34
- # Avoid the use of attr. Use attr_reader and attr_accessor instead.
35
- Attr:
36
- Enabled: false
37
-
38
32
  UnusedBlockArgument:
39
33
  AutoCorrect: false
40
34
 
@@ -44,31 +38,33 @@ UnusedMethodArgument:
44
38
  CyclomaticComplexity:
45
39
  Max: 30
46
40
 
47
- # https://github.com/bbatsov/ruby-style-guide/issues/395
48
- RaiseArgs:
49
- Enabled: false
50
-
51
41
  AbcSize:
52
42
  Max: 20
53
43
 
54
- TrivialAccessors:
55
- AllowPredicates: true
44
+
45
+ # Indentation
56
46
 
57
47
  AlignHash:
58
48
  EnforcedHashRocketStyle: table
59
49
  EnforcedColonStyle: table
60
50
 
61
- IndentArray:
51
+ IndentFirstArrayElement:
52
+ EnforcedStyle: consistent
53
+
54
+ IndentFirstHashElement:
62
55
  EnforcedStyle: consistent
63
56
 
64
57
  SignalException:
65
58
  EnforcedStyle: semantic
66
59
 
67
- #MultilineMethodCallIndentation:
68
- # EnforcedStyle: indented
69
- #
70
- #MultilineOperationIndentation:
71
- # EnforcedStyle: indented
72
- #
73
- #AlignParameters:
74
- # Enabled: false
60
+ IndentFirstArgument:
61
+ EnforcedStyle: consistent
62
+
63
+ MultilineMethodCallIndentation:
64
+ EnforcedStyle: indented
65
+
66
+ MultilineOperationIndentation:
67
+ EnforcedStyle: indented
68
+
69
+ AlignParameters:
70
+ EnforcedStyle: with_fixed_indentation
data/Gemfile CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  source 'https://rubygems.org'
2
4
 
3
5
  gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,73 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ subconv (1.0.0)
5
+ solid-struct (~> 0.1)
6
+ timecode (~> 2.2)
7
+
8
+ GEM
9
+ remote: https://rubygems.org/
10
+ specs:
11
+ ansi (1.5.0)
12
+ approximately (1.1.0)
13
+ ast (2.4.0)
14
+ diff-lcs (1.3)
15
+ docile (1.3.2)
16
+ jaro_winkler (1.5.3)
17
+ json (2.2.0)
18
+ parallel (1.17.0)
19
+ parser (2.6.3.0)
20
+ ast (~> 2.4.0)
21
+ rainbow (3.0.0)
22
+ rake (12.3.2)
23
+ rspec (3.8.0)
24
+ rspec-core (~> 3.8.0)
25
+ rspec-expectations (~> 3.8.0)
26
+ rspec-mocks (~> 3.8.0)
27
+ rspec-core (3.8.2)
28
+ rspec-support (~> 3.8.0)
29
+ rspec-expectations (3.8.4)
30
+ diff-lcs (>= 1.2.0, < 2.0)
31
+ rspec-support (~> 3.8.0)
32
+ rspec-mocks (3.8.1)
33
+ diff-lcs (>= 1.2.0, < 2.0)
34
+ rspec-support (~> 3.8.0)
35
+ rspec-support (3.8.2)
36
+ rubocop (0.72.0)
37
+ jaro_winkler (~> 1.5.1)
38
+ parallel (~> 1.10)
39
+ parser (>= 2.6)
40
+ rainbow (>= 2.2.2, < 4.0)
41
+ ruby-progressbar (~> 1.7)
42
+ unicode-display_width (>= 1.4.0, < 1.7)
43
+ ruby-progressbar (1.10.1)
44
+ simplecov (0.17.0)
45
+ docile (~> 1.1)
46
+ json (>= 1.8, < 3)
47
+ simplecov-html (~> 0.10.0)
48
+ simplecov-console (0.5.0)
49
+ ansi
50
+ simplecov
51
+ terminal-table
52
+ simplecov-html (0.10.2)
53
+ solid-struct (0.1.0)
54
+ terminal-table (1.8.0)
55
+ unicode-display_width (~> 1.1, >= 1.1.1)
56
+ timecode (2.2.2)
57
+ approximately (~> 1.1)
58
+ unicode-display_width (1.6.0)
59
+
60
+ PLATFORMS
61
+ ruby
62
+
63
+ DEPENDENCIES
64
+ bundler (~> 1.7)
65
+ rake (~> 12.3)
66
+ rspec (~> 3.8.0)
67
+ rubocop (~> 0.72)
68
+ simplecov (~> 0.17)
69
+ simplecov-console (~> 0.5)
70
+ subconv!
71
+
72
+ BUNDLED WITH
73
+ 1.16.1
data/README.md CHANGED
@@ -1,9 +1,3 @@
1
- [![Gem Version](https://badge.fury.io/rb/subconv.svg)](http://badge.fury.io/rb/subconv)
2
- [![Dependency Status](https://gemnasium.com/pkerling/subconv.svg)](https://gemnasium.com/pkerling/subconv)
3
- [![Build Status](https://travis-ci.org/pkerling/subconv.svg?branch=master)](https://travis-ci.org/pkerling/subconv)
4
- [![Coverage Status](https://coveralls.io/repos/github/pkerling/subconv/badge.svg?branch=master)](https://coveralls.io/github/pkerling/subconv?branch=master)
5
- [![Inline docs](http://inch-ci.org/github/pkerling/subconv.svg?branch=master)](http://inch-ci.org/github/pkerling/subconv)
6
-
7
1
  subconv - Ruby SCC (EIA-608) to WebVTT subtitle converter
8
2
  =========================================================
9
3
 
@@ -26,6 +20,7 @@ Usage
26
20
  -c, --no-color Remove all color information from output
27
21
  -F, --no-flash Remove all flash (blinking) information from output
28
22
  -s, --simple-positions Convert to simple top/bottom center-aligned captions
23
+ -p, --keep-paint-on Keep paint-on captions, i.e. do not combine them into pop-on cues
29
24
  -h, --help Show this help message and quit.
30
25
 
31
26
  The API can also be used programmatically, the `bin/subconv` file is just an
@@ -35,12 +30,15 @@ Supported features
35
30
  ------------------
36
31
  * EIA-608 parsing and conversion to WebVTT
37
32
  * Pop-on captions
33
+ * Paint-on captions
38
34
  * Full positioning
39
- * All special characters defined in the standard
35
+ * All special characters defined in the FCC standard
36
+ * All extended (Western European) characters defined in EIA-608-B
40
37
  * All colors
41
38
  * Italics
42
39
  * Underline
43
40
  * Flash
41
+ * Paint-on to pop-on caption conversion (by combining similar cues)
44
42
  * Optional removal of certain features during the conversion
45
43
  * Color
46
44
  * Flash
@@ -55,5 +53,4 @@ Firefox can not apply classes/styles inside cues at all.
55
53
  Unsupported features
56
54
  --------------------
57
55
  * Roll-up captions
58
- * Paint-on captions
59
56
  * EIA-708
data/Rakefile CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'bundler/gem_tasks'
2
4
  require 'rspec/core/rake_task'
3
5
  require 'rubocop/rake_task'
@@ -7,4 +9,4 @@ RuboCop::RakeTask.new do |task|
7
9
  task.options = ['--fail-level', 'warning']
8
10
  end
9
11
 
10
- task default: [:spec, :rubocop]
12
+ task default: %i[spec rubocop]
data/bin/subconv CHANGED
@@ -21,6 +21,9 @@ OptionParser.new do |opts|
21
21
  opts.on('-s', '--simple-positions', 'Convert to simple top/bottom center-aligned captions') do
22
22
  options[:simple_positions] = true
23
23
  end
24
+ opts.on('-p', '--keep-paint-on', 'Keep paint-on captions, i.e. do not combine them into pop-on cues') do
25
+ options[:keep_paint_on] = true
26
+ end
24
27
  opts.on_tail('-h', '--help', 'Show this help message and quit.') do
25
28
  puts opts.help
26
29
  exit
@@ -35,7 +38,10 @@ reader = Subconv::Scc::Reader.new
35
38
  File.open(options[:in_file], File::RDONLY) do |file|
36
39
  reader.read(file, options[:fps].to_f)
37
40
  end
38
- captions = Subconv::Scc::Transformer.new.transform reader.captions
41
+ transformer = Subconv::Scc::Transformer.new
42
+ scc_captions = reader.captions
43
+ scc_captions = transformer.combine_paint_on_captions(scc_captions) unless options[:keep_paint_on]
44
+ captions = transformer.transform scc_captions
39
45
  filter_options = {}
40
46
  filter_options[:remove_color] = true if options[:no_color]
41
47
  filter_options[:remove_flash] = true if options[:no_flash]
data/lib/subconv.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'subconv/version'
2
4
  require 'subconv/scc/reader'
3
5
  require 'subconv/scc/transformer'
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Subconv
2
4
  # Two-dimensional screen position relative (both x and y position between 0 and 1) to the screen size
3
5
  class Position
@@ -17,6 +19,7 @@ module Subconv
17
19
  def x=(x)
18
20
  x = x.to_f
19
21
  fail RangeError, 'X position not between 0 and 1' unless x.between?(0.0, 1.0)
22
+
20
23
  @x = x
21
24
  end
22
25
 
@@ -24,6 +27,7 @@ module Subconv
24
27
  def y=(y)
25
28
  y = y.to_f
26
29
  fail RangeError, 'Y position not between 0 and 1' unless y.between?(0.0, 1.0)
30
+
27
31
  @y = y
28
32
  end
29
33
  end
@@ -58,6 +62,7 @@ module Subconv
58
62
 
59
63
  def children=(children)
60
64
  fail 'Children must be an array' unless children.class == Array
65
+
61
66
  @children = children
62
67
  end
63
68
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'subconv/caption'
2
4
 
3
5
  module Subconv
@@ -65,9 +67,7 @@ module Subconv
65
67
  # Remove nils resulting from removed captions
66
68
  captions.compact!
67
69
  # Do per-caption processing after merging etc.
68
- captions.each do |caption|
69
- process_caption!(caption)
70
- end
70
+ captions.each(&method(:process_caption!))
71
71
  end
72
72
 
73
73
  def process_caption!(caption)
@@ -88,7 +88,9 @@ module Subconv
88
88
  if current_text_node.nil?
89
89
  current_text_node = child
90
90
  else
91
- # Add text to previous node
91
+ # Add text to previous node, copying the text if necessary (literals are frozen by default)
92
+ current_text_node.text = current_text_node.text.dup if current_text_node.text.frozen?
93
+
92
94
  current_text_node.text << child.text
93
95
  # Remove this node
94
96
  next
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  require 'subconv/utility'
3
4
 
4
5
  require 'solid_struct'
@@ -6,7 +7,7 @@ require 'timecode'
6
7
 
7
8
  module Subconv
8
9
  module Scc
9
- FILE_MAGIC = 'Scenarist_SCC V1.0'.freeze
10
+ FILE_MAGIC = 'Scenarist_SCC V1.0'
10
11
 
11
12
  # Grid size
12
13
  GRID_ROWS = 15
@@ -14,8 +15,18 @@ module Subconv
14
15
 
15
16
  # Grid is just an array with some extra convenience functions and a default size
16
17
  class Grid < Array
17
- def initialize
18
- super(GRID_ROWS) { Array.new(GRID_COLUMNS) }
18
+ def initialize(*args)
19
+ if args.empty?
20
+ super(GRID_ROWS) { Array.new(GRID_COLUMNS) }
21
+ elsif args.size == 1
22
+ fail ArgumentError, 'Attempted to create grid from unsupported type' unless args[0].is_a?(Array)
23
+ fail ArgumentError, 'Grid has illegal row count' if args[0].size != GRID_ROWS
24
+ fail ArgumentError, 'Grid has illegal column count' if args[0].any? { |row| row.size != GRID_COLUMNS }
25
+
26
+ super(args.first)
27
+ else
28
+ fail ArgumentError, 'Illegal number of parameters'
29
+ end
19
30
  end
20
31
 
21
32
  # The grid is empty when there are no characters in it
@@ -23,6 +34,11 @@ module Subconv
23
34
  flatten.compact.empty?
24
35
  end
25
36
 
37
+ # Make a deep copy
38
+ def clone
39
+ Grid.new(map { |row| row.map(&:clone) })
40
+ end
41
+
26
42
  # Insert continuous text at a given position
27
43
  # Returns self for chaining
28
44
  def insert_text(row, column, text, style = CharacterStyle.default)
@@ -32,6 +48,21 @@ module Subconv
32
48
  end
33
49
  self
34
50
  end
51
+
52
+ def without_identical_characters(other_grid)
53
+ result = clone
54
+ each_with_index do |row, row_i|
55
+ row.each_with_index do |column, column_i|
56
+ result[row_i][column_i] = nil if other_grid[row_i][column_i] == column
57
+ end
58
+ end
59
+
60
+ result
61
+ end
62
+
63
+ def to_simple_text
64
+ map { |row| row.map { |char| char.nil? ? ' ' : char.character }.join }.join("\n")
65
+ end
35
66
  end
36
67
 
37
68
  # Color constants as immutable value objects with some convenience functions (e.g. conversion to string or symbol)
@@ -92,6 +123,7 @@ module Subconv
92
123
  def self.for_value(value)
93
124
  color = COLORS[value]
94
125
  fail "Color value #{value} is unknown" if color.nil?
126
+
95
127
  color
96
128
  end
97
129
 
@@ -111,7 +143,45 @@ module Subconv
111
143
  Character = SolidStruct.new(:character, :style)
112
144
 
113
145
  # One fully rendered caption displayed at a specific point in time
114
- Caption = SolidStruct.new(:timecode, :grid)
146
+ class Caption
147
+ def initialize(args)
148
+ # Inject default
149
+ args = { mode: :pop_on, char_replacement: false }.merge(args)
150
+ self.timecode = args[:timecode]
151
+ self.grid = args[:grid]
152
+ self.mode = args[:mode]
153
+ self.char_replacement = args[:char_replacement]
154
+ end
155
+
156
+ def ==(other)
157
+ timecode == other.timecode && grid == other.grid && mode == other.mode && char_replacement? == other.char_replacement?
158
+ end
159
+
160
+ alias eql? ==
161
+
162
+ attr_reader :mode
163
+ attr_writer :char_replacement
164
+ attr_accessor :timecode, :grid
165
+
166
+ def mode=(mode)
167
+ fail 'Unknown mode' unless %i[pop_on paint_on].include?(mode)
168
+
169
+ @mode = mode
170
+ end
171
+
172
+ def pop_on_mode?
173
+ @mode == :pop_on
174
+ end
175
+
176
+ def paint_on_mode?
177
+ @mode == :paint_on
178
+ end
179
+
180
+ # Is this caption just replacing an existing character with an extended character?
181
+ def char_replacement?
182
+ @char_replacement
183
+ end
184
+ end
115
185
 
116
186
  # SCC reader
117
187
  # Parse and render an SCC file sequentially into a background and foreground grid
@@ -122,23 +192,24 @@ module Subconv
122
192
  # The advanced recovery methods mentioned in CEA608 are not implemented since the source is assumed to contain no errors (e.g. DVD source).
123
193
  class Reader
124
194
  # Regular expression for parsing one line of data
125
- LINE_REGEXP = /^(?<timecode>[0-9:;]+)\t(?<data>(?:[0-9a-fA-F]{4} ?)+)$/
195
+ LINE_REGEXP = /^(?<timecode>[0-9:;]+)\t(?<data>(?:[0-9a-fA-F]{4} ?)+)$/.freeze
126
196
 
127
197
  # rubocop:disable MutableConstant
128
198
 
129
199
  # Map of standard characters that do not match the standard ASCII codes
130
200
  # to their corresponding unicode characters
131
201
  STANDARD_CHARACTER_MAP = {
132
- '*' => "\u00e1",
133
- '\\' => "\u00e9",
134
- '^' => "\u00ed",
135
- '_' => "\u00f3",
136
- '`' => "\u00fa",
137
- '{' => "\u00e7",
138
- '|' => "\u00f7",
139
- '}' => "\u00d1",
140
- '~' => "\u00f1",
141
- "\x7f" => "\u2588"
202
+ '\'' => '’', # CEA-608-B says to use curly apostrophe
203
+ '*' => 'á',
204
+ '\\' => 'é',
205
+ '^' => 'í',
206
+ '_' => 'ó',
207
+ '`' => 'ú',
208
+ '{' => 'ç',
209
+ '|' => '÷',
210
+ '}' => 'Ñ',
211
+ '~' => 'ñ',
212
+ "\x7f" => '█'
142
213
  }
143
214
  # rubocop:enable MutableConstant
144
215
  # Simply return the character if no exception matched
@@ -148,24 +219,14 @@ module Subconv
148
219
  STANDARD_CHARACTER_MAP.freeze
149
220
 
150
221
  # Map of special characters to unicode codepoints
151
- SPECIAL_CHARACTER_MAP = {
152
- '0' => "\u00ae",
153
- '1' => "\u00b0",
154
- '2' => "\u00bd",
155
- '3' => "\u00bf",
156
- '4' => "\u2122",
157
- '5' => "\u00a2",
158
- '6' => "\u00a3",
159
- '7' => "\u266a",
160
- '8' => "\u00e0",
161
- # "\x39" => transparent space is handled specially since it is not a real character
162
- ':' => "\u00e8",
163
- ';' => "\u00e2",
164
- '<' => "\u00ea",
165
- '=' => "\u00ee",
166
- '>' => "\u00f4",
167
- '?' => "\u00fb"
168
- }.freeze
222
+ # 0x39 transparent space is handled specially since it is not a real character
223
+ SPECIAL_CHARACTER_MAP = Hash[%w[® ° ½ ¿ ™ ¢ £ ♪ à _ è á ê î ô û].each_with_index.map { |char, i| [0x30 + i, char] }].freeze
224
+
225
+ # Extended character maps for Western European languages and box drawing
226
+ EXTENDED_CHARACTER_MAPS = [
227
+ Hash[%w[Á É Ó Ú Ü ü ‘ ¡ * ' © ℠ · “ ” À Â Ç È Ê Ë ë Î Ï ï Ô Ù ù Û « »].each_with_index.map { |char, i| [0x20 + i, char] }].freeze,
228
+ Hash[%w[Ã ã Í Ì ì Ò ò Õ õ { } \\ ^ _ | ~ Ä ä Ö ö ß ¥ ¤ │ Å å Ø ø ┌ ┐ └ ┘].each_with_index.map { |char, i| [0x20 + i, char] }].freeze
229
+ ].freeze
169
230
 
170
231
  # Map of preamble address code high bytes to their
171
232
  # corresponding base row numbers (counted from 0)
@@ -185,16 +246,17 @@ module Subconv
185
246
  class InvalidFormatError < Error; end
186
247
  class ParityError < Error; end
187
248
 
188
- # Internal state of the parser consisting of current drawing position and character style
249
+ # Internal state of the parser consisting of current drawing position, character style and captioning mode
189
250
  class State
190
251
  def initialize(params)
191
252
  self.row = params[:row]
192
253
  self.column = params[:column]
193
254
  @style = params[:style]
255
+ self.mode = params[:mode]
194
256
  end
195
257
 
196
258
  attr_accessor :style
197
- attr_reader :row, :column
259
+ attr_reader :row, :column, :mode, :char_replaced
198
260
 
199
261
  # Make sure the maximum row count is not exceeded
200
262
  def row=(row)
@@ -206,8 +268,32 @@ module Subconv
206
268
  @column = Utility.clamp(column, 0, GRID_COLUMNS - 1)
207
269
  end
208
270
 
271
+ def mode=(mode)
272
+ fail 'Unknown mode' unless %i[pop_on paint_on].include?(mode)
273
+
274
+ @mode = mode
275
+ end
276
+
277
+ def pop_on_mode?
278
+ @mode == :pop_on
279
+ end
280
+
281
+ def paint_on_mode?
282
+ @mode == :paint_on
283
+ end
284
+
285
+ def char_replaced=(replaced)
286
+ fail ArgumentError, 'Invalid value for char_replaced' unless [true, false].include?(replaced)
287
+
288
+ @char_replaced = replaced
289
+ end
290
+
291
+ def start_new_frame
292
+ self.char_replaced = false
293
+ end
294
+
209
295
  def self.default
210
- State.new(row: 0, column: 0, style: CharacterStyle.default)
296
+ State.new(row: 0, column: 0, style: CharacterStyle.default, mode: :pop_on)
211
297
  end
212
298
  end
213
299
 
@@ -225,6 +311,8 @@ module Subconv
225
311
  @now = Timecode.new(0, fps)
226
312
  @data_channel = 0
227
313
 
314
+ update_active_grid
315
+
228
316
  magic = io.readline.chomp!
229
317
  fail InvalidFormatError, 'File does not start with "' + Scc::FILE_MAGIC + '"' unless Scc::FILE_MAGIC == magic
230
318
 
@@ -235,6 +323,7 @@ module Subconv
235
323
 
236
324
  line_data = LINE_REGEXP.match(line)
237
325
  fail InvalidFormatError, "Invalid line \"#{line}\"" if line_data.nil?
326
+
238
327
  # Parse timecode
239
328
  old_time = @now
240
329
  timecode = Timecode.new(line_data[:timecode], fps)
@@ -254,10 +343,13 @@ module Subconv
254
343
 
255
344
  data.split(' ').each do |word_string|
256
345
  begin
346
+ @state.start_new_frame
347
+
257
348
  # Decode hexadecimal word into two-byte string
258
349
  word = [word_string].pack('H*')
259
350
  # Check parity
260
351
  fail ParityError, "At least one byte in word #{word_string} has even parity, odd required" unless !check_parity || (correct_parity?(word[0]) && correct_parity?(word[1]))
352
+
261
353
  # Remove parity bit for further processing
262
354
  word = word.bytes.collect { |byte|
263
355
  # Unset 8th bit
@@ -298,15 +390,20 @@ module Subconv
298
390
  # for further processing.
299
391
  end
300
392
 
301
- if hi == 0x11 && lo >= 0x30 && lo <= 0x3f
393
+ # rubocop:disable Style/NumericPredicate
394
+
395
+ if hi == 0x11 && (0x30..0x3f).cover?(lo)
302
396
  # Special character
303
397
  handle_special_character(lo)
304
- elsif hi >= 0x10 && hi <= 0x17 && lo >= 0x40
305
- # Premable address code
398
+ elsif (0x12..0x13).cover?(hi) && (0x20..0x3f).cover?(lo)
399
+ # Extended character
400
+ handle_extended_character(hi & 1, lo)
401
+ elsif (0x10..0x17).cover?(hi) && (0x40..0xff).cover?(lo)
402
+ # Preamble address code
306
403
  handle_preamble_address_code(hi, lo)
307
- elsif (hi == 0x14 || hi == 0x17) && lo >= 0x20 && lo <= 0x2f
404
+ elsif [0x14, 0x17].include?(hi) && (0x20..0x2f).cover?(lo)
308
405
  handle_control_code(hi, lo)
309
- elsif hi == 0x11 && lo >= 0x20 && lo <= 0x2f
406
+ elsif hi == 0x11 && (0x20..0x2f).cover?(lo)
310
407
  handle_mid_row_code(hi, lo)
311
408
  elsif hi == 0x00 && lo == 0x00
312
409
  # Ignore filler
@@ -314,9 +411,12 @@ module Subconv
314
411
  puts "Ignoring unknown command #{hi}/#{lo}"
315
412
  end
316
413
 
414
+ # rubocop:enable Style/NumericPredicate
415
+
317
416
  last_command = word
318
417
  end
319
418
 
419
+ post_frame if @state.paint_on_mode?
320
420
  ensure
321
421
  # Advance one frame for each word read
322
422
  @now += 1
@@ -327,14 +427,14 @@ module Subconv
327
427
  # Insert one unicode character into the grid at the current position and with the
328
428
  # current style, then advance the cursor one column
329
429
  def insert_character(char)
330
- @background_grid[@state.row][@state.column] = Character.new(char, @state.style.dup)
430
+ @active_grid[@state.row][@state.column] = Character.new(char, @state.style.dup)
331
431
  @state.column += 1
332
432
  end
333
433
 
334
434
  # Insert a CEA608 character into the grid at the current position, converting it to its unicode representation
335
435
  def handle_character(byte)
336
436
  # Ignore filler character
337
- return if byte == 0
437
+ return if byte.zero?
338
438
 
339
439
  char = STANDARD_CHARACTER_MAP[byte.chr]
340
440
  insert_character(char)
@@ -345,17 +445,26 @@ module Subconv
345
445
  def handle_special_character(byte)
346
446
  if byte == 0x39
347
447
  # Transparent space: Move cursor after deleting the current column to open up a hole
348
- @background_grid[@state.row][@state.column] = nil
448
+ @active_grid[@state.row][@state.column] = nil
349
449
  @state.column += 1
350
450
  else
351
- char = SPECIAL_CHARACTER_MAP[byte.chr]
451
+ char = SPECIAL_CHARACTER_MAP.fetch(byte)
352
452
  insert_character(char)
353
453
  end
354
454
  end
355
455
 
456
+ # Insert an extended character into the grid at the current position, converting it to its Unicode representation
457
+ def handle_extended_character(map, byte)
458
+ char = EXTENDED_CHARACTER_MAPS.fetch(map).fetch(byte)
459
+ # Extended characters include automatic backspace+overwrite
460
+ @state.column -= 1
461
+ insert_character(char)
462
+ @state.char_replaced = true
463
+ end
464
+
356
465
  # Set drawing position and style according to the information in a preamble address code
357
466
  def handle_preamble_address_code(hi, lo)
358
- @state.row = PREAMBLE_ADDRESS_CODE_ROW_MAP[hi]
467
+ @state.row = PREAMBLE_ADDRESS_CODE_ROW_MAP.fetch(hi)
359
468
  # Low byte bit 5 adds 1 to the row number if set
360
469
  @state.row += 1 if lo & (1 << 5) != 0
361
470
 
@@ -375,12 +484,16 @@ module Subconv
375
484
  @state.style.color = Color::WHITE
376
485
  # One indent equals 4 characters
377
486
  @state.column = color_or_indent * 4
378
- elsif color_or_indent == 7
379
- # "color" 7 is white with italics
380
- @state.style.color = Color::WHITE
381
- @state.style.italics = true
382
487
  else
383
- @state.style.color = Color.for_value(color_or_indent)
488
+ # Style code always sets first column
489
+ @state.column = 0
490
+ if color_or_indent == 7
491
+ # "color" 7 is white with italics
492
+ @state.style.color = Color::WHITE
493
+ @state.style.italics = true
494
+ else
495
+ @state.style.color = Color.for_value(color_or_indent)
496
+ end
384
497
  end
385
498
  end
386
499
 
@@ -388,38 +501,49 @@ module Subconv
388
501
  def handle_control_code(hi, lo)
389
502
  if hi == 0x14 && lo == 0x20
390
503
  # Resume caption loading
391
- # Nothing to do here, only pop-onstyle is supported anyway
504
+ @state.mode = :pop_on
505
+ update_active_grid
392
506
  elsif hi == 0x14 && lo == 0x21
393
507
  # Backspace
394
508
  unless @state.column.zero? # Ignore in the first column
395
509
  @state.column -= 1
396
510
  # Delete character at cursor after moving one character back
397
- @background_grid[@state.row][@state.column] = nil
511
+ @active_grid[@state.row][@state.column] = nil
398
512
  end
399
513
  elsif hi == 0x14 && lo == 0x24
400
514
  # Delete to end of row
401
515
  (@state.column...GRID_COLUMNS).each do |column|
402
- @background_grid[@state.row][column] = nil
516
+ @active_grid[@state.row][column] = nil
403
517
  end
404
518
  elsif hi == 0x14 && lo == 0x28
405
519
  # Flash on
406
520
  # Flash is a spacing character
407
521
  insert_character(' ')
408
522
  @state.style.flash = true
523
+ elsif hi == 0x14 && lo == 0x29
524
+ # Resume direct captioning
525
+ @state.mode = :paint_on
526
+ update_active_grid
409
527
  # elsif hi == 0x14 && lo == 0x2b
410
- # Resume text display -> not a pop-on command
528
+ # Resume text display -> command not described in spec
411
529
  # fail "RTD"
412
530
  elsif hi == 0x14 && lo == 0x2c
413
531
  # Erase displayed memory
414
532
  @foreground_grid = Grid.new
415
533
  post_frame
534
+ update_active_grid
416
535
  elsif hi == 0x14 && lo == 0x2e
417
536
  # Erase non-displayed memory
418
537
  @background_grid = Grid.new
538
+ update_active_grid
419
539
  elsif hi == 0x14 && lo == 0x2f
420
540
  # End of caption (flip memories)
421
541
  @foreground_grid, @background_grid = @background_grid, @foreground_grid
542
+ # This also forces pop-on mode
543
+ @state.mode = :pop_on
422
544
  post_frame
545
+
546
+ update_active_grid
423
547
  elsif hi == 0x17 && lo >= 0x21 && lo <= 0x23
424
548
  # Tab offset
425
549
  # Bits 0 and 1 designate how many columns to go
@@ -452,13 +576,17 @@ module Subconv
452
576
  # Insert the currently displayed foreground grid as caption into the captions array
453
577
  # Must be called whenever the foreground grid is changed as a result of a command
454
578
  def post_frame
455
- # Only push a new caption if the grid has changed
456
- if @captions.empty? || @foreground_grid != @last_grid
457
- # Save space by not saving the grid if it is completely empty
458
- grid = @foreground_grid.empty? ? nil : @foreground_grid
459
- @captions.push(Caption.new(timecode: @now, grid: grid))
460
- @last_grid = @foreground_grid
461
- end
579
+ # Only push a new caption if the grid has changed, but do not push out an empty grid initially
580
+ return unless @foreground_grid != @last_grid && !(@last_grid.nil? && @foreground_grid.empty?)
581
+
582
+ # Save space by not saving the grid if it is completely empty
583
+ grid = @foreground_grid.empty? ? nil : @foreground_grid
584
+ @captions.push(Caption.new(timecode: @now, grid: grid.clone, mode: @state.mode, char_replacement: @state.char_replaced))
585
+ @last_grid = @foreground_grid.clone
586
+ end
587
+
588
+ def update_active_grid
589
+ @active_grid = @state.paint_on_mode? ? @foreground_grid : @background_grid
462
590
  end
463
591
 
464
592
  # Check a byte for odd parity