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 +5 -5
- data/.gitignore +1 -1
- data/.gitlab-ci.yml +33 -0
- data/.rubocop.yml +26 -30
- data/Gemfile +2 -0
- data/Gemfile.lock +73 -0
- data/README.md +5 -8
- data/Rakefile +3 -1
- data/bin/subconv +7 -1
- data/lib/subconv.rb +2 -0
- data/lib/subconv/caption.rb +5 -0
- data/lib/subconv/caption_filter.rb +6 -4
- data/lib/subconv/scc/reader.rb +190 -62
- data/lib/subconv/scc/transformer.rb +64 -33
- data/lib/subconv/utility.rb +4 -1
- data/lib/subconv/version.rb +3 -1
- data/lib/subconv/webvtt/writer.rb +44 -33
- data/spec/.rubocop.yml +8 -0
- data/spec/caption_filter_spec.rb +2 -0
- data/spec/scc/reader_spec.rb +140 -65
- data/spec/scc/transformer_spec.rb +168 -48
- data/spec/spec_helper.rb +8 -2
- data/spec/test_helpers.rb +18 -0
- data/spec/webvtt/writer_spec.rb +16 -14
- data/subconv.gemspec +11 -11
- metadata +38 -35
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 6348b3b43fd4b191b25d166007dd539d18d3e72c13f32cb23e7feb839c06d665
|
4
|
+
data.tar.gz: 8a0b8177d72cf42ff45649e28d27c333ad223072193181bcb023a6349c8e6ee5
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d805a1a29e705a9fbc23ec481484600f99ef844fc51422469a1768d255d2894e8c7c06fe9c45079cf97b99a86b5bbb34df0c7c09083ac5cd493d084ebce605a7
|
7
|
+
data.tar.gz: bfbe89e2915ffc77484521d8e291cb8f58685dc8e5bf819c34c62bb8344b03c187dfcedce55363003ab1565d7eb59694f4cee1aee85c2a9e9b9cee84cb1d6ac6
|
data/.gitignore
CHANGED
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
|
+
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
|
-
|
28
|
-
|
29
|
-
|
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
|
-
|
55
|
-
|
44
|
+
|
45
|
+
# Indentation
|
56
46
|
|
57
47
|
AlignHash:
|
58
48
|
EnforcedHashRocketStyle: table
|
59
49
|
EnforcedColonStyle: table
|
60
50
|
|
61
|
-
|
51
|
+
IndentFirstArrayElement:
|
52
|
+
EnforcedStyle: consistent
|
53
|
+
|
54
|
+
IndentFirstHashElement:
|
62
55
|
EnforcedStyle: consistent
|
63
56
|
|
64
57
|
SignalException:
|
65
58
|
EnforcedStyle: semantic
|
66
59
|
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
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
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: [
|
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
|
-
|
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
data/lib/subconv/caption.rb
CHANGED
@@ -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
|
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
|
data/lib/subconv/scc/reader.rb
CHANGED
@@ -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'
|
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
|
-
|
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
|
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
|
-
'
|
133
|
-
'
|
134
|
-
'
|
135
|
-
'
|
136
|
-
'
|
137
|
-
'
|
138
|
-
'
|
139
|
-
'
|
140
|
-
'
|
141
|
-
|
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
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
'
|
157
|
-
|
158
|
-
|
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
|
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
|
-
|
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
|
305
|
-
#
|
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
|
404
|
+
elsif [0x14, 0x17].include?(hi) && (0x20..0x2f).cover?(lo)
|
308
405
|
handle_control_code(hi, lo)
|
309
|
-
elsif hi == 0x11 &&
|
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
|
-
@
|
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
|
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
|
-
@
|
448
|
+
@active_grid[@state.row][@state.column] = nil
|
349
449
|
@state.column += 1
|
350
450
|
else
|
351
|
-
char = SPECIAL_CHARACTER_MAP
|
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
|
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
|
-
|
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
|
-
|
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
|
-
@
|
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
|
-
@
|
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
|
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
|
-
|
457
|
-
|
458
|
-
|
459
|
-
|
460
|
-
|
461
|
-
|
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
|