titlekit 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +17 -0
- data/.travis.yml +3 -0
- data/Gemfile +3 -0
- data/LICENSE +22 -0
- data/README.md +335 -0
- data/Rakefile +8 -0
- data/lib/titlekit/have.rb +90 -0
- data/lib/titlekit/job.rb +446 -0
- data/lib/titlekit/parsers/ass.rb +175 -0
- data/lib/titlekit/parsers/ass.treetop +72 -0
- data/lib/titlekit/parsers/srt.rb +139 -0
- data/lib/titlekit/parsers/srt.treetop +73 -0
- data/lib/titlekit/parsers/ssa.rb +201 -0
- data/lib/titlekit/parsers/ssa.treetop +72 -0
- data/lib/titlekit/specification.rb +131 -0
- data/lib/titlekit/utilities.rb +3 -0
- data/lib/titlekit/version.rb +3 -0
- data/lib/titlekit/want.rb +24 -0
- data/lib/titlekit.rb +9 -0
- data/spec/ass_spec.rb +113 -0
- data/spec/automatic_grouping/automatic_grouping_spec.rb +55 -0
- data/spec/automatic_grouping/dual_tracks/expected.srt +15 -0
- data/spec/automatic_grouping/dual_tracks/one.srt +11 -0
- data/spec/automatic_grouping/dual_tracks/out.srt +15 -0
- data/spec/automatic_grouping/dual_tracks/two.srt +11 -0
- data/spec/automatic_grouping/single_track/expected.srt +24 -0
- data/spec/automatic_grouping/single_track/one.srt +11 -0
- data/spec/automatic_grouping/single_track/out.srt +24 -0
- data/spec/automatic_grouping/single_track/two.srt +11 -0
- data/spec/encoding_detection/a/in.ass +0 -0
- data/spec/encoding_detection/b/in.srt +2389 -0
- data/spec/encoding_detection/b/out.srt +2389 -0
- data/spec/encoding_detection/c/in.srt +5320 -0
- data/spec/encoding_detection/c/out.srt +5320 -0
- data/spec/encoding_detection/encoding_detection_spec.rb +81 -0
- data/spec/files/ass/authentic.ass +0 -0
- data/spec/files/ass/hard.ass +37 -0
- data/spec/files/ass/simple.ass +28 -0
- data/spec/files/srt/authentic.srt +2708 -0
- data/spec/files/srt/coordinates.srt +13 -0
- data/spec/files/srt/simple.srt +12 -0
- data/spec/files/ssa/simple.ssa +26 -0
- data/spec/files/try/unsupported-output.try +0 -0
- data/spec/files/try/unsupported.try +7 -0
- data/spec/format_conversion/ass_srt/expected.srt +2327 -0
- data/spec/format_conversion/ass_srt/in.ass +485 -0
- data/spec/format_conversion/ass_srt/out.srt +2327 -0
- data/spec/format_conversion/format_conversion_spec.rb +112 -0
- data/spec/format_conversion/srt_ass/expected.ass +19 -0
- data/spec/format_conversion/srt_ass/in.srt +12 -0
- data/spec/format_conversion/srt_ass/out.ass +19 -0
- data/spec/format_conversion/srt_ssa/expected.ssa +19 -0
- data/spec/format_conversion/srt_ssa/in.srt +12 -0
- data/spec/format_conversion/srt_ssa/out.ssa +19 -0
- data/spec/format_conversion/ssa_srt/expected.srt +9 -0
- data/spec/format_conversion/ssa_srt/in.ssa +26 -0
- data/spec/format_conversion/ssa_srt/out.srt +9 -0
- data/spec/job_spec.rb +162 -0
- data/spec/simultaneous_subtitles/dual/ass/expected.ass +22 -0
- data/spec/simultaneous_subtitles/dual/ass/out.ass +22 -0
- data/spec/simultaneous_subtitles/dual/one.srt +11 -0
- data/spec/simultaneous_subtitles/dual/srt/expected.srt +27 -0
- data/spec/simultaneous_subtitles/dual/srt/out.srt +27 -0
- data/spec/simultaneous_subtitles/dual/ssa/expected.ssa +22 -0
- data/spec/simultaneous_subtitles/dual/ssa/out.ssa +22 -0
- data/spec/simultaneous_subtitles/dual/two.srt +11 -0
- data/spec/simultaneous_subtitles/simultaneous_subtitles_spec.rb +220 -0
- data/spec/simultaneous_subtitles/triple/ass/expected.ass +25 -0
- data/spec/simultaneous_subtitles/triple/ass/out.ass +25 -0
- data/spec/simultaneous_subtitles/triple/one.srt +11 -0
- data/spec/simultaneous_subtitles/triple/srt/expected.srt +55 -0
- data/spec/simultaneous_subtitles/triple/srt/out.srt +55 -0
- data/spec/simultaneous_subtitles/triple/ssa/expected.ssa +25 -0
- data/spec/simultaneous_subtitles/triple/ssa/out.ssa +25 -0
- data/spec/simultaneous_subtitles/triple/three.srt +11 -0
- data/spec/simultaneous_subtitles/triple/two.srt +11 -0
- data/spec/simultaneous_subtitles/triple_plus/ass/expected.ass +93 -0
- data/spec/simultaneous_subtitles/triple_plus/ass/out.ass +93 -0
- data/spec/simultaneous_subtitles/triple_plus/five.srt +11 -0
- data/spec/simultaneous_subtitles/triple_plus/four.srt +11 -0
- data/spec/simultaneous_subtitles/triple_plus/one.srt +11 -0
- data/spec/simultaneous_subtitles/triple_plus/six.srt +11 -0
- data/spec/simultaneous_subtitles/triple_plus/srt/expected.srt +149 -0
- data/spec/simultaneous_subtitles/triple_plus/srt/out.srt +149 -0
- data/spec/simultaneous_subtitles/triple_plus/ssa/expected.ssa +93 -0
- data/spec/simultaneous_subtitles/triple_plus/ssa/out.ssa +93 -0
- data/spec/simultaneous_subtitles/triple_plus/three.srt +11 -0
- data/spec/simultaneous_subtitles/triple_plus/two.srt +11 -0
- data/spec/spec_helper.rb +7 -0
- data/spec/specifications_spec.rb +138 -0
- data/spec/srt_spec.rb +134 -0
- data/spec/ssa_spec.rb +90 -0
- data/spec/timecode_correction/double_reference/expected.srt +13 -0
- data/spec/timecode_correction/double_reference/in.srt +12 -0
- data/spec/timecode_correction/double_reference/out.srt +13 -0
- data/spec/timecode_correction/framerate/expected.srt +5 -0
- data/spec/timecode_correction/framerate/in.srt +4 -0
- data/spec/timecode_correction/framerate/out.srt +5 -0
- data/spec/timecode_correction/framerate_plus_reference/expected.srt +13 -0
- data/spec/timecode_correction/framerate_plus_reference/in.srt +12 -0
- data/spec/timecode_correction/framerate_plus_reference/out.srt +13 -0
- data/spec/timecode_correction/single_reference/expected.srt +13 -0
- data/spec/timecode_correction/single_reference/in.srt +12 -0
- data/spec/timecode_correction/single_reference/out.srt +13 -0
- data/spec/timecode_correction/timecode_correction_spec.rb +124 -0
- data/spec/transcoding/gb2312-ascii/in.srt +12 -0
- data/spec/transcoding/iso-8859-1_utf-8/expected.srt +12 -0
- data/spec/transcoding/iso-8859-1_utf-8/in.srt +11 -0
- data/spec/transcoding/iso-8859-1_utf-8/out.srt +12 -0
- data/spec/transcoding/transcoding_spec.rb +116 -0
- data/spec/transcoding/utf-8_gbk/expected.srt +12 -0
- data/spec/transcoding/utf-8_gbk/in.srt +11 -0
- data/spec/transcoding/utf-8_gbk/out.srt +12 -0
- data/spec/transcoding/windows-1252_utf-8/expected.srt +12 -0
- data/spec/transcoding/windows-1252_utf-8/in.srt +11 -0
- data/spec/transcoding/windows-1252_utf-8/out.srt +12 -0
- data/titlekit.gemspec +28 -0
- metadata +313 -0
@@ -0,0 +1,139 @@
|
|
1
|
+
require 'treetop'
|
2
|
+
|
3
|
+
module Titlekit
|
4
|
+
module SRT
|
5
|
+
|
6
|
+
class Subtitles < Treetop::Runtime::SyntaxNode
|
7
|
+
def build
|
8
|
+
elements.map { |subtitle| subtitle.build }
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
class Subtitle < Treetop::Runtime::SyntaxNode
|
13
|
+
def build
|
14
|
+
{
|
15
|
+
id: id.text_value.to_i,
|
16
|
+
start: from.build,
|
17
|
+
end: to.build,
|
18
|
+
lines: lines.text_value.rstrip
|
19
|
+
}
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
class Timecode < Treetop::Runtime::SyntaxNode
|
24
|
+
def build
|
25
|
+
value = 0
|
26
|
+
value += hours.text_value.to_i * 3600
|
27
|
+
value += minutes.text_value.to_i * 60
|
28
|
+
value += seconds.text_value.to_i
|
29
|
+
value += "0.#{fractions.text_value}".to_f
|
30
|
+
value
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
# Parses the supplied string and returns the results.
|
35
|
+
#
|
36
|
+
#
|
37
|
+
def self.import(string)
|
38
|
+
Treetop.load(File.join(__dir__, 'srt'))
|
39
|
+
parser = SRTParser.new
|
40
|
+
syntax_tree = parser.parse(string)
|
41
|
+
|
42
|
+
if syntax_tree
|
43
|
+
return syntax_tree.build
|
44
|
+
else
|
45
|
+
failure = "failure_index #{parser.failure_index}\n"
|
46
|
+
failure += "failure_line #{parser.failure_line}\n"
|
47
|
+
failure += "failure_column #{parser.failure_column}\n"
|
48
|
+
failure += "failure_reason #{parser.failure_reason}\n"
|
49
|
+
|
50
|
+
raise failure
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
# Master the subtitles for best possible usage of the format's features.
|
55
|
+
#
|
56
|
+
# @param subtitles [Array<Hash>] the subtitles to master
|
57
|
+
def self.master(subtitles)
|
58
|
+
tracks = subtitles.map { |subtitle| subtitle[:track] }.uniq
|
59
|
+
|
60
|
+
if tracks.length == 1
|
61
|
+
|
62
|
+
# maybe styling? aside that: nada más!
|
63
|
+
|
64
|
+
elsif tracks.length >= 2
|
65
|
+
|
66
|
+
mastered_subtitles = []
|
67
|
+
|
68
|
+
# Determine timeframes with a discrete state
|
69
|
+
cuts = subtitles.map { |s| [s[:start], s[:end]] }.flatten.uniq.sort
|
70
|
+
frames = []
|
71
|
+
cuts.each_cons(2) do |pair|
|
72
|
+
frames << { start: pair[0], end: pair[1] }
|
73
|
+
end
|
74
|
+
|
75
|
+
frames.each do |frame|
|
76
|
+
intersecting = subtitles.select do |subtitle|
|
77
|
+
(subtitle[:end] == frame[:end] || subtitle[:start] == frame[:start] ||
|
78
|
+
(subtitle[:start] < frame[:start] && subtitle[:end] > frame[:end]))
|
79
|
+
end
|
80
|
+
|
81
|
+
if intersecting.any?
|
82
|
+
intersecting.sort_by! { |subtitle| tracks.index(subtitle[:track]) }
|
83
|
+
|
84
|
+
subtitle = {}
|
85
|
+
subtitle[:id] = mastered_subtitles.length+1
|
86
|
+
subtitle[:start] = frame[:start]
|
87
|
+
subtitle[:end] = frame[:end]
|
88
|
+
|
89
|
+
# Combine two or more than three simultaneous tracks by
|
90
|
+
# stacking them directly, with different colors.
|
91
|
+
|
92
|
+
colored_lines = intersecting.map do |subtitle|
|
93
|
+
color = DEFAULT_PALETTE[tracks.index(subtitle[:track]) % DEFAULT_PALETTE.length]
|
94
|
+
"<font color=\"##{color}\">#{subtitle[:lines]}</font>"
|
95
|
+
end
|
96
|
+
|
97
|
+
subtitle[:lines] = colored_lines.join("\n")
|
98
|
+
|
99
|
+
mastered_subtitles << subtitle
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
subtitles.replace(mastered_subtitles)
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def self.export(subtitles)
|
108
|
+
result = ''
|
109
|
+
|
110
|
+
subtitles.each_with_index do |subtitle, index|
|
111
|
+
result << (index+1).to_s
|
112
|
+
result << "\n"
|
113
|
+
result << SRT.build_timecode(subtitle[:start])
|
114
|
+
result << ' --> '
|
115
|
+
result << SRT.build_timecode(subtitle[:end])
|
116
|
+
result << "\n"
|
117
|
+
result << subtitle[:lines]
|
118
|
+
result << "\n\n"
|
119
|
+
end
|
120
|
+
|
121
|
+
return result
|
122
|
+
end
|
123
|
+
|
124
|
+
protected
|
125
|
+
|
126
|
+
def self.build_timecode(seconds)
|
127
|
+
sprintf("%02d:%02d:%02d,%s",
|
128
|
+
seconds / 3600,
|
129
|
+
(seconds%3600) / 60,
|
130
|
+
seconds % 60,
|
131
|
+
sprintf("%.3f", seconds)[-3, 3])
|
132
|
+
end
|
133
|
+
|
134
|
+
def self.parse_timecode(timecode)
|
135
|
+
mres = timecode.match(/(?<h>\d+):(?<m>\d+):(?<s>\d+),(?<ms>\d+)/)
|
136
|
+
"#{mres["h"].to_i * 3600 + mres["m"].to_i * 60 + mres["s"].to_i}.#{mres["ms"]}".to_f
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
grammar SRT
|
2
|
+
rule subtitles
|
3
|
+
subtitle* <Titlekit::SRT::Subtitles>
|
4
|
+
end
|
5
|
+
|
6
|
+
rule subtitle
|
7
|
+
id eol from ' --> ' to coordinates? eol lines eos <Titlekit::SRT::Subtitle>
|
8
|
+
end
|
9
|
+
|
10
|
+
rule id
|
11
|
+
number
|
12
|
+
end
|
13
|
+
|
14
|
+
rule from
|
15
|
+
timecode
|
16
|
+
end
|
17
|
+
|
18
|
+
rule to
|
19
|
+
timecode
|
20
|
+
end
|
21
|
+
|
22
|
+
rule timecode
|
23
|
+
hours ':' minutes ':' seconds (',' / '.') fractions <Titlekit::SRT::Timecode>
|
24
|
+
end
|
25
|
+
|
26
|
+
rule hours
|
27
|
+
number
|
28
|
+
end
|
29
|
+
|
30
|
+
rule minutes
|
31
|
+
number
|
32
|
+
end
|
33
|
+
|
34
|
+
rule seconds
|
35
|
+
number
|
36
|
+
end
|
37
|
+
|
38
|
+
rule fractions
|
39
|
+
number
|
40
|
+
end
|
41
|
+
|
42
|
+
rule coordinates
|
43
|
+
' X1:' number ' X2:' number ' Y1:' number ' Y2:' number
|
44
|
+
end
|
45
|
+
|
46
|
+
rule lines
|
47
|
+
line+
|
48
|
+
end
|
49
|
+
|
50
|
+
rule line
|
51
|
+
string (eol / eof)
|
52
|
+
end
|
53
|
+
|
54
|
+
rule eos
|
55
|
+
eol+ / eof
|
56
|
+
end
|
57
|
+
|
58
|
+
rule eol
|
59
|
+
"\r\n" / "\n" / "\r"
|
60
|
+
end
|
61
|
+
|
62
|
+
rule eof
|
63
|
+
!.
|
64
|
+
end
|
65
|
+
|
66
|
+
rule number
|
67
|
+
[0-9]+
|
68
|
+
end
|
69
|
+
|
70
|
+
rule string
|
71
|
+
(!eol .)+
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,201 @@
|
|
1
|
+
require 'treetop'
|
2
|
+
|
3
|
+
module Titlekit
|
4
|
+
module SSA
|
5
|
+
|
6
|
+
class Subtitles < Treetop::Runtime::SyntaxNode
|
7
|
+
def build
|
8
|
+
event_section.events.build
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
class ScriptInfo < Treetop::Runtime::SyntaxNode
|
13
|
+
def build
|
14
|
+
# elements.map { |subtitle| subtitle.build }
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
class V4PStyles < Treetop::Runtime::SyntaxNode
|
19
|
+
def build
|
20
|
+
# elements.map { |subtitle| subtitle.build }
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
class Events < Treetop::Runtime::SyntaxNode
|
25
|
+
def build
|
26
|
+
elements.map do |line|
|
27
|
+
subtitle = {}
|
28
|
+
|
29
|
+
fields = line.text_value.split(',')
|
30
|
+
|
31
|
+
subtitle[:id] = elements.index(line) + 1
|
32
|
+
subtitle[:start] = SSA.parse_timecode(fields[1])
|
33
|
+
subtitle[:end] = SSA.parse_timecode(fields[2])
|
34
|
+
subtitle[:lines] = fields[9..-1].join.gsub('\N', "\n")
|
35
|
+
|
36
|
+
subtitle
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# class Subtitle < Treetop::Runtime::SyntaxNode
|
42
|
+
# def build
|
43
|
+
# {
|
44
|
+
# id: id.text_value.to_i,
|
45
|
+
# start: from.build,
|
46
|
+
# end: to.build,
|
47
|
+
# lines: lines.text_value.rstrip
|
48
|
+
# }
|
49
|
+
# end
|
50
|
+
# end
|
51
|
+
|
52
|
+
# class Timecode < Treetop::Runtime::SyntaxNode
|
53
|
+
# def build
|
54
|
+
# value = 0
|
55
|
+
# value += hours.text_value.to_i * 3600
|
56
|
+
# value += minutes.text_value.to_i * 60
|
57
|
+
# value += seconds.text_value.to_i
|
58
|
+
# value += "0.#{fractions.text_value}".to_f
|
59
|
+
# value
|
60
|
+
# end
|
61
|
+
# end
|
62
|
+
|
63
|
+
# Parses the supplied string and returns the results.
|
64
|
+
#
|
65
|
+
#
|
66
|
+
def self.import(string)
|
67
|
+
Treetop.load(File.join(__dir__, 'ssa'))
|
68
|
+
parser = SSAParser.new
|
69
|
+
syntax_tree = parser.parse(string)
|
70
|
+
|
71
|
+
if syntax_tree
|
72
|
+
return syntax_tree.build
|
73
|
+
else
|
74
|
+
failure = "failure_index #{parser.failure_index}\n"
|
75
|
+
failure += "failure_line #{parser.failure_line}\n"
|
76
|
+
failure += "failure_column #{parser.failure_column}\n"
|
77
|
+
failure += "failure_reason #{parser.failure_reason}\n"
|
78
|
+
|
79
|
+
raise failure
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
# Master the subtitles for best possible usage of the format's features.
|
84
|
+
#
|
85
|
+
# @param subtitles [Array<Hash>] the subtitles to master
|
86
|
+
def self.master(subtitles)
|
87
|
+
tracks = subtitles.map { |subtitle| subtitle[:track] }.uniq
|
88
|
+
|
89
|
+
if tracks.length == 1
|
90
|
+
|
91
|
+
# maybe styling? aside that: nada más!
|
92
|
+
|
93
|
+
elsif (2..3).include?(tracks.length)
|
94
|
+
|
95
|
+
subtitles.each do |subtitle|
|
96
|
+
case tracks.index(subtitle[:track])
|
97
|
+
when 0
|
98
|
+
subtitle[:style] = 'Default'
|
99
|
+
when 1
|
100
|
+
subtitle[:style] = 'Top'
|
101
|
+
when 2
|
102
|
+
subtitle[:style] = 'Middle'
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
elsif tracks.length >= 4
|
107
|
+
|
108
|
+
mastered_subtitles = []
|
109
|
+
|
110
|
+
# Determine timeframes with a discrete state
|
111
|
+
cuts = subtitles.map { |s| [s[:start], s[:end]] }.flatten.uniq.sort
|
112
|
+
frames = []
|
113
|
+
cuts.each_cons(2) do |pair|
|
114
|
+
frames << { start: pair[0], end: pair[1] }
|
115
|
+
end
|
116
|
+
|
117
|
+
frames.each do |frame|
|
118
|
+
intersecting = subtitles.select do |subtitle|
|
119
|
+
(subtitle[:end] == frame[:end] || subtitle[:start] == frame[:start] ||
|
120
|
+
(subtitle[:start] < frame[:start] && subtitle[:end] > frame[:end]))
|
121
|
+
end
|
122
|
+
|
123
|
+
if intersecting.any?
|
124
|
+
intersecting.sort_by! { |subtitle| tracks.index(subtitle[:track]) }
|
125
|
+
intersecting.each do |subtitle|
|
126
|
+
new_subtitle = {}
|
127
|
+
new_subtitle[:id] = mastered_subtitles.length+1
|
128
|
+
new_subtitle[:start] = frame[:start]
|
129
|
+
new_subtitle[:end] = frame[:end]
|
130
|
+
|
131
|
+
color = DEFAULT_PALETTE[tracks.index(subtitle[:track]) % DEFAULT_PALETTE.length]
|
132
|
+
new_subtitle[:style] = color
|
133
|
+
new_subtitle[:lines] = subtitle[:lines]
|
134
|
+
|
135
|
+
mastered_subtitles << new_subtitle
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
subtitles.replace(mastered_subtitles)
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
def self.export(subtitles)
|
145
|
+
result = ''
|
146
|
+
|
147
|
+
result << "[Script Info]\nScriptType: v4.00\n\n"
|
148
|
+
|
149
|
+
result << "[V4 Styles]\nFormat: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, TertiaryColour, BackColour, Bold, Italic, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, AlphaLevel, Encoding\n"
|
150
|
+
result << "Style: Default,Arial,16,16777215,16777215,16777215,-2147483640,0,0,1,3,0,2,70,70,40,0,0\n"
|
151
|
+
result << "Style: Middle,Arial,16,16777215,16777215,16777215,-2147483640,0,0,1,3,0,10,70,70,40,0,0\n"
|
152
|
+
result << "Style: Top,Arial,16,16777215,16777215,16777215,-2147483640,0,0,1,3,0,6,70,70,40,0,0\n"
|
153
|
+
|
154
|
+
DEFAULT_PALETTE.each do |color|
|
155
|
+
# reordered_color = ""
|
156
|
+
# reordered_color << color[4..5]
|
157
|
+
# reordered_color << color[2..3]
|
158
|
+
# reordered_color << color[0..1]\
|
159
|
+
processed_color = (color[4..5]+color[2..3]+color[0..1]).to_i(16)
|
160
|
+
result << "Style: #{color},Arial,16,#{processed_color},#{processed_color},#{processed_color},-2147483640,0,0,1,3,0,2,70,70,40,0,0\n"
|
161
|
+
end
|
162
|
+
|
163
|
+
result << "\n" # Close styles section
|
164
|
+
|
165
|
+
result << "[Events]\nFormat: Marked, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text\n"
|
166
|
+
subtitles.each do |subtitle|
|
167
|
+
fields = [
|
168
|
+
'Dialogue: 0', # Format: Marked
|
169
|
+
SSA.build_timecode(subtitle[:start]), # Start
|
170
|
+
SSA.build_timecode(subtitle[:end]), # End
|
171
|
+
subtitle[:style] || 'Default', # Style
|
172
|
+
'', # Name
|
173
|
+
'0000', # MarginL
|
174
|
+
'0000', # MarginR
|
175
|
+
'0000', # MarginV
|
176
|
+
'',# Effect
|
177
|
+
subtitle[:lines].gsub("\n", '\N') # Text
|
178
|
+
]
|
179
|
+
|
180
|
+
result << (fields.join(',') + "\n")
|
181
|
+
end
|
182
|
+
|
183
|
+
return result
|
184
|
+
end
|
185
|
+
|
186
|
+
protected
|
187
|
+
|
188
|
+
def self.build_timecode(seconds)
|
189
|
+
sprintf("%01d:%02d:%02d.%s",
|
190
|
+
seconds / 3600,
|
191
|
+
(seconds%3600) / 60,
|
192
|
+
seconds % 60,
|
193
|
+
sprintf("%.2f", seconds)[-2, 3])
|
194
|
+
end
|
195
|
+
|
196
|
+
def self.parse_timecode(timecode)
|
197
|
+
mres = timecode.match(/(?<h>\d):(?<m>\d{2}):(?<s>\d{2})[:|\.](?<ms>\d+)/)
|
198
|
+
return "#{mres["h"].to_i * 3600 + mres["m"].to_i * 60 + mres["s"].to_i}.#{mres["ms"]}".to_f
|
199
|
+
end
|
200
|
+
end
|
201
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
grammar SSA
|
2
|
+
rule subtitles
|
3
|
+
info?
|
4
|
+
styles?
|
5
|
+
event_section
|
6
|
+
fonts?
|
7
|
+
graphics?
|
8
|
+
end_of_data
|
9
|
+
|
10
|
+
<Titlekit::SSA::Subtitles>
|
11
|
+
end
|
12
|
+
|
13
|
+
rule info
|
14
|
+
'[Script Info]' end_of_line lines* end_of_section <Titlekit::SSA::ScriptInfo>
|
15
|
+
end
|
16
|
+
|
17
|
+
rule styles
|
18
|
+
'[V4 Styles]' end_of_line lines* end_of_section <Titlekit::SSA::V4PStyles>
|
19
|
+
end
|
20
|
+
|
21
|
+
rule event_section
|
22
|
+
'[Events]' end_of_line line events end_of_section
|
23
|
+
end
|
24
|
+
|
25
|
+
rule events
|
26
|
+
line* <Titlekit::SSA::Events>
|
27
|
+
end
|
28
|
+
|
29
|
+
rule event
|
30
|
+
'Dialogue'
|
31
|
+
end
|
32
|
+
|
33
|
+
rule fonts
|
34
|
+
'[Fonts]' end_of_line lines* end_of_section
|
35
|
+
end
|
36
|
+
|
37
|
+
rule graphics
|
38
|
+
'[Graphics]' end_of_line lines* end_of_section
|
39
|
+
end
|
40
|
+
|
41
|
+
rule lines
|
42
|
+
line+
|
43
|
+
end
|
44
|
+
|
45
|
+
rule line
|
46
|
+
string (end_of_line / end_of_file)
|
47
|
+
end
|
48
|
+
|
49
|
+
rule end_of_section
|
50
|
+
end_of_line+ / end_of_file
|
51
|
+
end
|
52
|
+
|
53
|
+
rule end_of_line
|
54
|
+
"\r\n" / "\n" / "\r"
|
55
|
+
end
|
56
|
+
|
57
|
+
rule end_of_data
|
58
|
+
end_of_line+ / end_of_file
|
59
|
+
end
|
60
|
+
|
61
|
+
rule end_of_file
|
62
|
+
!.
|
63
|
+
end
|
64
|
+
|
65
|
+
rule number
|
66
|
+
[0-9]+
|
67
|
+
end
|
68
|
+
|
69
|
+
rule string
|
70
|
+
(!end_of_line .)+
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,131 @@
|
|
1
|
+
module Titlekit
|
2
|
+
class Specification
|
3
|
+
|
4
|
+
# Only for internal usage by the job control center
|
5
|
+
attr_accessor :subtitles
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
@encoding = nil
|
9
|
+
@file = nil
|
10
|
+
@fps = nil
|
11
|
+
@references = {}
|
12
|
+
@subtitles = []
|
13
|
+
@track = nil
|
14
|
+
|
15
|
+
return self
|
16
|
+
end
|
17
|
+
|
18
|
+
# @param [String] A string specifying the encoding, e.g. 'utf-8' or 'ISO-8859-1'
|
19
|
+
# @return If you omit the argument, it returns the already specified encoding
|
20
|
+
def encoding(*args)
|
21
|
+
if args.empty?
|
22
|
+
return @encoding
|
23
|
+
else
|
24
|
+
@encoding = args[0]
|
25
|
+
return self
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
# @param [String] A string specifying the path to the file
|
30
|
+
# @return If you omit the argument, it returns the already specified path
|
31
|
+
def file(*args)
|
32
|
+
if args.empty?
|
33
|
+
return @file
|
34
|
+
else
|
35
|
+
@file = args[0]
|
36
|
+
return self
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# @param [String] A string specifying the track identifier
|
41
|
+
# @return If you omit the argument, it returns the already specified track
|
42
|
+
def track(*args)
|
43
|
+
if args.empty?
|
44
|
+
return @track
|
45
|
+
else
|
46
|
+
@track = args[0]
|
47
|
+
return self
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
# @param [Float] A float specifying the frames per second, e.g. 23.976
|
52
|
+
# @return If you omit the argument, it returns the already specified fps
|
53
|
+
def fps(*args)
|
54
|
+
if args.empty?
|
55
|
+
return @fps
|
56
|
+
else
|
57
|
+
@fps = args[0]
|
58
|
+
return self
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
# Returns all named references you have specified
|
63
|
+
def references
|
64
|
+
return @references
|
65
|
+
end
|
66
|
+
|
67
|
+
# Places a named reference (in the form of a string or a symbol)
|
68
|
+
# on a timecode specified by either +hours+, +minutes+, +seconds+
|
69
|
+
# or +milliseconds+.
|
70
|
+
#
|
71
|
+
# Its typical use-case is to reference a specific subtitle you can
|
72
|
+
# recognize in both the movie and your subtitle file, where usually
|
73
|
+
# for the subtitle file (represented by {Have}) you will reference
|
74
|
+
# the subtitle index and for the movie (represented by {Want}) you
|
75
|
+
# will reference the timecode that is displayed when the line occurs
|
76
|
+
# in the movie.
|
77
|
+
#
|
78
|
+
# @example Referencing a timecode by hours
|
79
|
+
# have.reference('Earl grey, hot', hours: 0.963)
|
80
|
+
#
|
81
|
+
# @example Referencing a timecode by seconds
|
82
|
+
# have.reference('In a galaxy ...', seconds: 14.2)
|
83
|
+
#
|
84
|
+
# @example Referencing a timecode by an SRT-style timecode
|
85
|
+
# have.reference('In a galaxy ...', srt_timecode: '00:00:14,200')
|
86
|
+
#
|
87
|
+
# @example Referencing a timecode by an ASS-style timecode
|
88
|
+
# have.reference('In a galaxy ...', ass_timecode: '0:00:14,20')
|
89
|
+
#
|
90
|
+
# @example Referencing a timecode by an SSA-style timecode
|
91
|
+
# have.reference('In a galaxy ...', ssa_timecode: '0:00:14,20')
|
92
|
+
#
|
93
|
+
# @example Symbols can be used as references as well!
|
94
|
+
# have.reference(:narrator_begins, minutes: 9.6)
|
95
|
+
#
|
96
|
+
# @param name [String, Symbol] The name of the reference
|
97
|
+
# @param hours [Float]
|
98
|
+
# @param minutes [Float]
|
99
|
+
# @param seconds [Float]
|
100
|
+
# @param milliseconds [Float]
|
101
|
+
def reference(name,
|
102
|
+
*args,
|
103
|
+
hours: nil,
|
104
|
+
minutes: nil,
|
105
|
+
seconds: nil,
|
106
|
+
milliseconds: nil,
|
107
|
+
srt_timecode: nil,
|
108
|
+
ssa_timecode: nil,
|
109
|
+
ass_timecode: nil)
|
110
|
+
|
111
|
+
@references[name] = case
|
112
|
+
when hours
|
113
|
+
{ timecode: hours * 3600 }
|
114
|
+
when minutes
|
115
|
+
{ timecode: minutes * 60 }
|
116
|
+
when seconds
|
117
|
+
{ timecode: seconds }
|
118
|
+
when milliseconds
|
119
|
+
{ timecode: milliseconds / 1000 }
|
120
|
+
when srt_timecode
|
121
|
+
{ timecode: Titlekit::SRT.parse_timecode(srt_timecode) }
|
122
|
+
when ssa_timecode
|
123
|
+
{ timecode: Titlekit::SSA.parse_timecode(ssa_timecode) }
|
124
|
+
when ass_timecode
|
125
|
+
{ timecode: Titlekit::ASS.parse_timecode(ass_timecode) }
|
126
|
+
end
|
127
|
+
|
128
|
+
return self
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Titlekit
|
2
|
+
class Want < Specification
|
3
|
+
|
4
|
+
def initialize
|
5
|
+
super
|
6
|
+
@glue_treshold = 0.08
|
7
|
+
end
|
8
|
+
|
9
|
+
# For dual subtitles the starts and ends of simultaneously occuring
|
10
|
+
# subtitles can be micro-adjusted together if their distance is smaller
|
11
|
+
# than the glue_treshold. Normally defaults to 0.08 (super-conservative).
|
12
|
+
#
|
13
|
+
# @param [Float] Specifies the new glue_treshold
|
14
|
+
# @return If you omit the argument, it returns the set glue_treshold
|
15
|
+
def glue_treshold(*args)
|
16
|
+
if args.empty?
|
17
|
+
return @glue_treshold
|
18
|
+
else
|
19
|
+
@glue_treshold = args[0]
|
20
|
+
return self
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
data/lib/titlekit.rb
ADDED
@@ -0,0 +1,9 @@
|
|
1
|
+
require 'titlekit/utilities'
|
2
|
+
require 'titlekit/parsers/ass'
|
3
|
+
require 'titlekit/parsers/ssa'
|
4
|
+
require 'titlekit/parsers/srt'
|
5
|
+
require 'titlekit/specification'
|
6
|
+
require 'titlekit/have'
|
7
|
+
require 'titlekit/want'
|
8
|
+
require 'titlekit/job'
|
9
|
+
require 'titlekit/version'
|