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.
Files changed (119) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +17 -0
  3. data/.travis.yml +3 -0
  4. data/Gemfile +3 -0
  5. data/LICENSE +22 -0
  6. data/README.md +335 -0
  7. data/Rakefile +8 -0
  8. data/lib/titlekit/have.rb +90 -0
  9. data/lib/titlekit/job.rb +446 -0
  10. data/lib/titlekit/parsers/ass.rb +175 -0
  11. data/lib/titlekit/parsers/ass.treetop +72 -0
  12. data/lib/titlekit/parsers/srt.rb +139 -0
  13. data/lib/titlekit/parsers/srt.treetop +73 -0
  14. data/lib/titlekit/parsers/ssa.rb +201 -0
  15. data/lib/titlekit/parsers/ssa.treetop +72 -0
  16. data/lib/titlekit/specification.rb +131 -0
  17. data/lib/titlekit/utilities.rb +3 -0
  18. data/lib/titlekit/version.rb +3 -0
  19. data/lib/titlekit/want.rb +24 -0
  20. data/lib/titlekit.rb +9 -0
  21. data/spec/ass_spec.rb +113 -0
  22. data/spec/automatic_grouping/automatic_grouping_spec.rb +55 -0
  23. data/spec/automatic_grouping/dual_tracks/expected.srt +15 -0
  24. data/spec/automatic_grouping/dual_tracks/one.srt +11 -0
  25. data/spec/automatic_grouping/dual_tracks/out.srt +15 -0
  26. data/spec/automatic_grouping/dual_tracks/two.srt +11 -0
  27. data/spec/automatic_grouping/single_track/expected.srt +24 -0
  28. data/spec/automatic_grouping/single_track/one.srt +11 -0
  29. data/spec/automatic_grouping/single_track/out.srt +24 -0
  30. data/spec/automatic_grouping/single_track/two.srt +11 -0
  31. data/spec/encoding_detection/a/in.ass +0 -0
  32. data/spec/encoding_detection/b/in.srt +2389 -0
  33. data/spec/encoding_detection/b/out.srt +2389 -0
  34. data/spec/encoding_detection/c/in.srt +5320 -0
  35. data/spec/encoding_detection/c/out.srt +5320 -0
  36. data/spec/encoding_detection/encoding_detection_spec.rb +81 -0
  37. data/spec/files/ass/authentic.ass +0 -0
  38. data/spec/files/ass/hard.ass +37 -0
  39. data/spec/files/ass/simple.ass +28 -0
  40. data/spec/files/srt/authentic.srt +2708 -0
  41. data/spec/files/srt/coordinates.srt +13 -0
  42. data/spec/files/srt/simple.srt +12 -0
  43. data/spec/files/ssa/simple.ssa +26 -0
  44. data/spec/files/try/unsupported-output.try +0 -0
  45. data/spec/files/try/unsupported.try +7 -0
  46. data/spec/format_conversion/ass_srt/expected.srt +2327 -0
  47. data/spec/format_conversion/ass_srt/in.ass +485 -0
  48. data/spec/format_conversion/ass_srt/out.srt +2327 -0
  49. data/spec/format_conversion/format_conversion_spec.rb +112 -0
  50. data/spec/format_conversion/srt_ass/expected.ass +19 -0
  51. data/spec/format_conversion/srt_ass/in.srt +12 -0
  52. data/spec/format_conversion/srt_ass/out.ass +19 -0
  53. data/spec/format_conversion/srt_ssa/expected.ssa +19 -0
  54. data/spec/format_conversion/srt_ssa/in.srt +12 -0
  55. data/spec/format_conversion/srt_ssa/out.ssa +19 -0
  56. data/spec/format_conversion/ssa_srt/expected.srt +9 -0
  57. data/spec/format_conversion/ssa_srt/in.ssa +26 -0
  58. data/spec/format_conversion/ssa_srt/out.srt +9 -0
  59. data/spec/job_spec.rb +162 -0
  60. data/spec/simultaneous_subtitles/dual/ass/expected.ass +22 -0
  61. data/spec/simultaneous_subtitles/dual/ass/out.ass +22 -0
  62. data/spec/simultaneous_subtitles/dual/one.srt +11 -0
  63. data/spec/simultaneous_subtitles/dual/srt/expected.srt +27 -0
  64. data/spec/simultaneous_subtitles/dual/srt/out.srt +27 -0
  65. data/spec/simultaneous_subtitles/dual/ssa/expected.ssa +22 -0
  66. data/spec/simultaneous_subtitles/dual/ssa/out.ssa +22 -0
  67. data/spec/simultaneous_subtitles/dual/two.srt +11 -0
  68. data/spec/simultaneous_subtitles/simultaneous_subtitles_spec.rb +220 -0
  69. data/spec/simultaneous_subtitles/triple/ass/expected.ass +25 -0
  70. data/spec/simultaneous_subtitles/triple/ass/out.ass +25 -0
  71. data/spec/simultaneous_subtitles/triple/one.srt +11 -0
  72. data/spec/simultaneous_subtitles/triple/srt/expected.srt +55 -0
  73. data/spec/simultaneous_subtitles/triple/srt/out.srt +55 -0
  74. data/spec/simultaneous_subtitles/triple/ssa/expected.ssa +25 -0
  75. data/spec/simultaneous_subtitles/triple/ssa/out.ssa +25 -0
  76. data/spec/simultaneous_subtitles/triple/three.srt +11 -0
  77. data/spec/simultaneous_subtitles/triple/two.srt +11 -0
  78. data/spec/simultaneous_subtitles/triple_plus/ass/expected.ass +93 -0
  79. data/spec/simultaneous_subtitles/triple_plus/ass/out.ass +93 -0
  80. data/spec/simultaneous_subtitles/triple_plus/five.srt +11 -0
  81. data/spec/simultaneous_subtitles/triple_plus/four.srt +11 -0
  82. data/spec/simultaneous_subtitles/triple_plus/one.srt +11 -0
  83. data/spec/simultaneous_subtitles/triple_plus/six.srt +11 -0
  84. data/spec/simultaneous_subtitles/triple_plus/srt/expected.srt +149 -0
  85. data/spec/simultaneous_subtitles/triple_plus/srt/out.srt +149 -0
  86. data/spec/simultaneous_subtitles/triple_plus/ssa/expected.ssa +93 -0
  87. data/spec/simultaneous_subtitles/triple_plus/ssa/out.ssa +93 -0
  88. data/spec/simultaneous_subtitles/triple_plus/three.srt +11 -0
  89. data/spec/simultaneous_subtitles/triple_plus/two.srt +11 -0
  90. data/spec/spec_helper.rb +7 -0
  91. data/spec/specifications_spec.rb +138 -0
  92. data/spec/srt_spec.rb +134 -0
  93. data/spec/ssa_spec.rb +90 -0
  94. data/spec/timecode_correction/double_reference/expected.srt +13 -0
  95. data/spec/timecode_correction/double_reference/in.srt +12 -0
  96. data/spec/timecode_correction/double_reference/out.srt +13 -0
  97. data/spec/timecode_correction/framerate/expected.srt +5 -0
  98. data/spec/timecode_correction/framerate/in.srt +4 -0
  99. data/spec/timecode_correction/framerate/out.srt +5 -0
  100. data/spec/timecode_correction/framerate_plus_reference/expected.srt +13 -0
  101. data/spec/timecode_correction/framerate_plus_reference/in.srt +12 -0
  102. data/spec/timecode_correction/framerate_plus_reference/out.srt +13 -0
  103. data/spec/timecode_correction/single_reference/expected.srt +13 -0
  104. data/spec/timecode_correction/single_reference/in.srt +12 -0
  105. data/spec/timecode_correction/single_reference/out.srt +13 -0
  106. data/spec/timecode_correction/timecode_correction_spec.rb +124 -0
  107. data/spec/transcoding/gb2312-ascii/in.srt +12 -0
  108. data/spec/transcoding/iso-8859-1_utf-8/expected.srt +12 -0
  109. data/spec/transcoding/iso-8859-1_utf-8/in.srt +11 -0
  110. data/spec/transcoding/iso-8859-1_utf-8/out.srt +12 -0
  111. data/spec/transcoding/transcoding_spec.rb +116 -0
  112. data/spec/transcoding/utf-8_gbk/expected.srt +12 -0
  113. data/spec/transcoding/utf-8_gbk/in.srt +11 -0
  114. data/spec/transcoding/utf-8_gbk/out.srt +12 -0
  115. data/spec/transcoding/windows-1252_utf-8/expected.srt +12 -0
  116. data/spec/transcoding/windows-1252_utf-8/in.srt +11 -0
  117. data/spec/transcoding/windows-1252_utf-8/out.srt +12 -0
  118. data/titlekit.gemspec +28 -0
  119. 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,3 @@
1
+ module Titlekit
2
+ DEFAULT_PALETTE = %w{EDF393 F5E665 FFC472 FFA891 89BABE}
3
+ end
@@ -0,0 +1,3 @@
1
+ module Titlekit
2
+ VERSION = '1.0.0'
3
+ 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'