titlekit 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 +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'
|