subconv 0.1.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml 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