dvdcc 0.1.0__py3-none-any.whl
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.
- dvdcc/__init__.py +10 -0
- dvdcc/__main__.py +52 -0
- dvdcc/captions.py +272 -0
- dvdcc/errors.py +9 -0
- dvdcc/subtitles.py +281 -0
- dvdcc-0.1.0.dist-info/METADATA +112 -0
- dvdcc-0.1.0.dist-info/RECORD +12 -0
- dvdcc-0.1.0.dist-info/WHEEL +5 -0
- dvdcc-0.1.0.dist-info/entry_points.txt +2 -0
- dvdcc-0.1.0.dist-info/licenses/LICENSE +202 -0
- dvdcc-0.1.0.dist-info/licenses/NOTICE +5 -0
- dvdcc-0.1.0.dist-info/top_level.txt +1 -0
dvdcc/__init__.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""dvdcc: line-21 closed captions (CEA-608, CC1) for DVD-Video.
|
|
2
|
+
|
|
3
|
+
Captions come from a Scenarist (.scc) file, used as it is, or from subtitles
|
|
4
|
+
(SubRip, WebVTT, ASS / SSA, text scripts) sent as pop-on captions, and go
|
|
5
|
+
into an MPEG-2 video elementary stream as the DVD's GOP user data ("CC"),
|
|
6
|
+
in place, without re-encoding. Author the disc with the titleset marked as
|
|
7
|
+
having line-21 captions (dvdauthor: <video caption="field1"/>).
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
__version__ = "0.1.0"
|
dvdcc/__main__.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Command line: `dvdcc VIDEO.m2v CAPTIONS [-o OUT] [--offset T] [--top]`."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import shutil
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
from . import __version__, captions
|
|
8
|
+
from .errors import CaptionError
|
|
9
|
+
from .subtitles import timecode_seconds
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _offset(value):
|
|
13
|
+
try:
|
|
14
|
+
return float(value)
|
|
15
|
+
except ValueError:
|
|
16
|
+
return timecode_seconds(value)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def main(argv=None):
|
|
20
|
+
p = argparse.ArgumentParser(
|
|
21
|
+
prog="dvdcc", description="Put line-21 closed captions (CEA-608, CC1) into the MPEG-2 "
|
|
22
|
+
"video of a DVD (29.97 fps NTSC), without re-encoding it.")
|
|
23
|
+
p.add_argument("--version", action="version", version=f"dvdcc {__version__}")
|
|
24
|
+
p.add_argument("video", help="an MPEG-2 video elementary stream (.m2v)")
|
|
25
|
+
p.add_argument("captions", help="a Scenarist .scc file, or subtitles: .srt, .vtt, .ass, "
|
|
26
|
+
".txt (text script)")
|
|
27
|
+
p.add_argument("-o", "--output", help="write here (default: change the video in place)")
|
|
28
|
+
p.add_argument("--offset", type=_offset, default=0.0,
|
|
29
|
+
help="taken off every time: seconds, or a timecode such as 01;00;00;00")
|
|
30
|
+
p.add_argument("--top", action="store_true", help="subtitles' captions at the top")
|
|
31
|
+
args = p.parse_args(argv)
|
|
32
|
+
try:
|
|
33
|
+
schedule, notes = captions.load(args.captions, args.offset,
|
|
34
|
+
"top" if args.top else "bottom")
|
|
35
|
+
target = args.output or args.video
|
|
36
|
+
if args.output:
|
|
37
|
+
shutil.copyfile(args.video, args.output)
|
|
38
|
+
frames = captions.inject_file(target, schedule)
|
|
39
|
+
except (CaptionError, ValueError) as e:
|
|
40
|
+
print(f"dvdcc: {getattr(e, 'message', e)}", file=sys.stderr)
|
|
41
|
+
return 1
|
|
42
|
+
except OSError as e:
|
|
43
|
+
print(f"dvdcc: {e.filename}: {e.strerror}", file=sys.stderr)
|
|
44
|
+
return 1
|
|
45
|
+
for n in notes:
|
|
46
|
+
print(f"dvdcc: note: {n}", file=sys.stderr)
|
|
47
|
+
print(f"{target}: captions over {frames} frames")
|
|
48
|
+
return 0
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
if __name__ == "__main__":
|
|
52
|
+
sys.exit(main())
|
dvdcc/captions.py
ADDED
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
"""Closed captions (line 21, CEA-608) on a DVD.
|
|
2
|
+
|
|
3
|
+
A DVD carries line-21 captions in its MPEG-2 video: user data after every
|
|
4
|
+
GOP header, "CC", then one caption word pair per frame of the GOP (in
|
|
5
|
+
display order) for each field. A caption decoder in the player or TV turns
|
|
6
|
+
them back into text. They come from
|
|
7
|
+
|
|
8
|
+
* a Scenarist (.scc) file: CEA-608 byte pairs with the timecode they start
|
|
9
|
+
at, as caption tools write them, used as they are; or
|
|
10
|
+
* subtitles (SubRip, WebVTT, ...): each one sent as a "pop-on" caption: the
|
|
11
|
+
text is loaded into the hidden memory before its time, shown at its start
|
|
12
|
+
(end of caption) and erased at its end.
|
|
13
|
+
|
|
14
|
+
Captions are NTSC only (line 21 of a 525-line picture), on 29.97 fps
|
|
15
|
+
video, field 1 (CC1).
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
import mmap
|
|
19
|
+
import os
|
|
20
|
+
import re
|
|
21
|
+
|
|
22
|
+
from . import subtitles
|
|
23
|
+
from .errors import CaptionError
|
|
24
|
+
|
|
25
|
+
FPS = 30000 / 1001
|
|
26
|
+
COLUMNS = 32
|
|
27
|
+
FILLER = (0x80, 0x80) # no data (two nulls with odd parity)
|
|
28
|
+
|
|
29
|
+
# preamble address code, first and second byte base, for rows 1..15
|
|
30
|
+
_PAC_ROWS = {1: (0x11, 0x40), 2: (0x11, 0x60), 3: (0x12, 0x40), 4: (0x12, 0x60),
|
|
31
|
+
5: (0x15, 0x40), 6: (0x15, 0x60), 7: (0x16, 0x40), 8: (0x16, 0x60),
|
|
32
|
+
9: (0x17, 0x40), 10: (0x17, 0x60), 11: (0x10, 0x40), 12: (0x13, 0x40),
|
|
33
|
+
13: (0x13, 0x60), 14: (0x14, 0x40), 15: (0x14, 0x60)}
|
|
34
|
+
# the basic character set is ASCII but for these
|
|
35
|
+
_SPECIAL = {"á": 0x2A, "é": 0x5C, "í": 0x5E, "ó": 0x5F, "ú": 0x60, "ç": 0x7B, "÷": 0x7C,
|
|
36
|
+
"Ñ": 0x7D, "ñ": 0x7E, "█": 0x7F}
|
|
37
|
+
_NOT_BASIC = set("*\\^_`{|}~")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def parity(b):
|
|
41
|
+
"""The byte with its odd-parity bit (bit 7)."""
|
|
42
|
+
b &= 0x7F
|
|
43
|
+
return b | 0x80 if bin(b).count("1") % 2 == 0 else b
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def pair(a, b):
|
|
47
|
+
return parity(a), parity(b)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
# CC1 control codes (first byte 0x14, field 1, data channel 1), with parity
|
|
51
|
+
RCL, EDM, ENM, EOC = (pair(0x14, x) for x in (0x20, 0x2C, 0x2E, 0x2F))
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def pac(row, column=0):
|
|
55
|
+
"""Preamble address code: row 1..15, white, indented to `column` (a
|
|
56
|
+
multiple of 4; the rest is padding with spaces)."""
|
|
57
|
+
first, base = _PAC_ROWS[row]
|
|
58
|
+
return pair(first, base + 0x10 + (column // 4) * 2 if column >= 4 else base)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def text_pairs(text):
|
|
62
|
+
"""Characters of one caption row as byte pairs; what the basic set has
|
|
63
|
+
no character for becomes a space. Returns (pairs, characters dropped)."""
|
|
64
|
+
codes, dropped = [], 0
|
|
65
|
+
for ch in text:
|
|
66
|
+
if ch in _SPECIAL:
|
|
67
|
+
codes.append(_SPECIAL[ch])
|
|
68
|
+
elif " " <= ch <= "~" and ch not in _NOT_BASIC:
|
|
69
|
+
codes.append(ord(ch))
|
|
70
|
+
else:
|
|
71
|
+
codes.append(0x20)
|
|
72
|
+
dropped += 1
|
|
73
|
+
if len(codes) % 2:
|
|
74
|
+
codes.append(0x00)
|
|
75
|
+
return [pair(a, b) for a, b in zip(codes[0::2], codes[1::2])], dropped
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def wrap(lines):
|
|
79
|
+
"""Caption rows of at most 32 characters (words wrapped), at most 4."""
|
|
80
|
+
rows = []
|
|
81
|
+
for line in lines:
|
|
82
|
+
words, cur = line.split(), ""
|
|
83
|
+
for w in words:
|
|
84
|
+
while len(w) > COLUMNS:
|
|
85
|
+
if cur:
|
|
86
|
+
rows.append(cur)
|
|
87
|
+
cur = ""
|
|
88
|
+
rows.append(w[:COLUMNS])
|
|
89
|
+
w = w[COLUMNS:]
|
|
90
|
+
if not cur:
|
|
91
|
+
cur = w
|
|
92
|
+
elif len(cur) + 1 + len(w) <= COLUMNS:
|
|
93
|
+
cur += " " + w
|
|
94
|
+
else:
|
|
95
|
+
rows.append(cur)
|
|
96
|
+
cur = w
|
|
97
|
+
if cur:
|
|
98
|
+
rows.append(cur)
|
|
99
|
+
return rows[:4]
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def frame_of(t):
|
|
103
|
+
return int(round(t * FPS))
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class Schedule:
|
|
107
|
+
"""Caption word pairs per frame (field 1)."""
|
|
108
|
+
|
|
109
|
+
def __init__(self):
|
|
110
|
+
self.frames = {}
|
|
111
|
+
|
|
112
|
+
def free(self, f):
|
|
113
|
+
return f not in self.frames
|
|
114
|
+
|
|
115
|
+
def put(self, f, p):
|
|
116
|
+
self.frames[f] = p
|
|
117
|
+
|
|
118
|
+
def put_from(self, f, pairs):
|
|
119
|
+
"""Put pairs on the free frames from `f` on; returns the last frame used."""
|
|
120
|
+
last = f
|
|
121
|
+
for p in pairs:
|
|
122
|
+
while not self.free(f):
|
|
123
|
+
f += 1
|
|
124
|
+
self.put(f, p)
|
|
125
|
+
last, f = f, f + 1
|
|
126
|
+
return last
|
|
127
|
+
|
|
128
|
+
def pairs(self, count):
|
|
129
|
+
return [self.frames.get(f, FILLER) for f in range(count)]
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def from_cues(cues, position="bottom"):
|
|
133
|
+
"""A Schedule from subtitles: one pop-on caption each. Returns
|
|
134
|
+
(schedule, notes)."""
|
|
135
|
+
s = Schedule()
|
|
136
|
+
notes = []
|
|
137
|
+
dropped = 0
|
|
138
|
+
shown_until = -1 # the frame the last caption was shown at (its EOC)
|
|
139
|
+
last_end = None
|
|
140
|
+
for c in cues:
|
|
141
|
+
rows = wrap(c.lines)
|
|
142
|
+
if not rows:
|
|
143
|
+
continue
|
|
144
|
+
load = [RCL, RCL, ENM, ENM]
|
|
145
|
+
top = (c.position or position) == "top"
|
|
146
|
+
first_row = 2 if top else 16 - len(rows)
|
|
147
|
+
for k, row in enumerate(rows):
|
|
148
|
+
col = (COLUMNS - len(row)) // 2
|
|
149
|
+
p = pac(first_row + k, col)
|
|
150
|
+
chars, d = text_pairs(" " * (col % 4) + row)
|
|
151
|
+
dropped += d
|
|
152
|
+
load += [p, p] + chars
|
|
153
|
+
start, end = frame_of(c.start), frame_of(c.end)
|
|
154
|
+
# load into the hidden memory after the previous caption went up
|
|
155
|
+
# (its EOC swaps the memories), ending just before this one's start
|
|
156
|
+
first = max(shown_until + 2, start - len(load) - 2)
|
|
157
|
+
loaded = s.put_from(first, load)
|
|
158
|
+
show = max(start, loaded + 1)
|
|
159
|
+
if show > start + 2:
|
|
160
|
+
notes.append(f"a caption at {c.start:.2f}s is {(show - start) / FPS:.2f}s late "
|
|
161
|
+
"(too much text since the previous one)")
|
|
162
|
+
if last_end is not None and last_end < show:
|
|
163
|
+
s.put_from(last_end, [EDM, EDM]) # the previous one goes at its end
|
|
164
|
+
shown_until = s.put_from(show, [EOC, EOC])
|
|
165
|
+
last_end = max(end, shown_until + 1)
|
|
166
|
+
if last_end is not None:
|
|
167
|
+
s.put_from(last_end, [EDM, EDM])
|
|
168
|
+
if dropped:
|
|
169
|
+
notes.append(f"{dropped} character(s) have no closed-caption character and became spaces")
|
|
170
|
+
return s, notes
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
_SCC_LINE = re.compile(r"^(\d{1,2}[:;]\d{2}[:;]\d{2}[:;.]\d{2})\s+((?:[0-9a-fA-F]{4}\s*)+)$")
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def parse_scc(text):
|
|
177
|
+
"""A Scenarist caption file -> Schedule (its byte pairs as they are,
|
|
178
|
+
from the frame of their timecode on)."""
|
|
179
|
+
lines = [line.strip() for line in text.splitlines() if line.strip()]
|
|
180
|
+
if not lines or not lines[0].startswith("Scenarist_SCC"):
|
|
181
|
+
raise ValueError("not a Scenarist caption file (no Scenarist_SCC header)")
|
|
182
|
+
s = Schedule()
|
|
183
|
+
for n, line in enumerate(lines[1:], 2):
|
|
184
|
+
m = _SCC_LINE.match(line)
|
|
185
|
+
if not m:
|
|
186
|
+
raise ValueError(f"line {n}: expected a timecode and byte pairs")
|
|
187
|
+
tc = m[1].replace(".", ";")
|
|
188
|
+
try:
|
|
189
|
+
f = frame_of(subtitles.timecode_seconds(tc, "NTSC"))
|
|
190
|
+
except ValueError as e:
|
|
191
|
+
raise ValueError(f"line {n}: {e}")
|
|
192
|
+
words = m[2].split()
|
|
193
|
+
s.put_from(f, [(int(w[:2], 16), int(w[2:], 16)) for w in words])
|
|
194
|
+
return s
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def load(path, offset=0.0, position="bottom", standard="NTSC"):
|
|
198
|
+
"""Captions of a file: .scc as they are, anything else as subtitles.
|
|
199
|
+
`offset` (seconds) is taken off every time. Returns (schedule, notes)."""
|
|
200
|
+
if path.lower().endswith(".scc"):
|
|
201
|
+
with open(path, encoding="utf-8-sig", errors="replace") as f:
|
|
202
|
+
try:
|
|
203
|
+
s = parse_scc(f.read())
|
|
204
|
+
except ValueError as e:
|
|
205
|
+
raise CaptionError(f"{path}: {e}")
|
|
206
|
+
if offset:
|
|
207
|
+
shift = frame_of(offset)
|
|
208
|
+
s.frames = {f - shift: p for f, p in s.frames.items() if f >= shift}
|
|
209
|
+
return s, []
|
|
210
|
+
cues = subtitles.shifted(subtitles.load_subtitles(path, standard), offset)
|
|
211
|
+
return from_cues(cues, position)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
# -- the MPEG-2 video stream ------------------------------------------------------
|
|
215
|
+
|
|
216
|
+
def gop_sizes(b):
|
|
217
|
+
"""Pictures in each GOP of an MPEG-2 elementary stream, in order."""
|
|
218
|
+
sizes, i = [], 0
|
|
219
|
+
while True:
|
|
220
|
+
i = b.find(b"\x00\x00\x01", i)
|
|
221
|
+
if i < 0 or i + 4 > len(b):
|
|
222
|
+
break
|
|
223
|
+
code = b[i + 3]
|
|
224
|
+
if code == 0xB8:
|
|
225
|
+
sizes.append(0)
|
|
226
|
+
elif code == 0x00 and sizes:
|
|
227
|
+
sizes[-1] += 1
|
|
228
|
+
i += 4
|
|
229
|
+
return sizes
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def user_data(pairs):
|
|
233
|
+
"""The DVD caption user data for a GOP: its frames' word pairs (field 1,
|
|
234
|
+
with field 2 empty), odd field first."""
|
|
235
|
+
n = len(pairs)
|
|
236
|
+
if n > 31:
|
|
237
|
+
raise ValueError("a GOP of more than 31 pictures cannot carry DVD captions")
|
|
238
|
+
out = bytearray(b"\x00\x00\x01\xb2CC\x01\xf8")
|
|
239
|
+
out.append(0x80 | (n << 1))
|
|
240
|
+
for a, b in pairs:
|
|
241
|
+
out += bytes([0xFF, a, b, 0xFE, 0x80, 0x80])
|
|
242
|
+
return bytes(out)
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def inject_file(path, schedule):
|
|
246
|
+
"""Write the captions into an MPEG-2 elementary stream (in place): after
|
|
247
|
+
each GOP header, the user data for that GOP's frames. Returns the number
|
|
248
|
+
of frames."""
|
|
249
|
+
with open(path, "rb") as f:
|
|
250
|
+
mm = mmap.mmap(f.fileno(), 0, access=mmap.ACCESS_READ)
|
|
251
|
+
try:
|
|
252
|
+
sizes = gop_sizes(mm)
|
|
253
|
+
frames = schedule.pairs(sum(sizes))
|
|
254
|
+
tmp = path + ".cc"
|
|
255
|
+
with open(tmp, "wb") as out:
|
|
256
|
+
pos = i = 0
|
|
257
|
+
k = frame = 0
|
|
258
|
+
while True:
|
|
259
|
+
i = mm.find(b"\x00\x00\x01\xb8", i)
|
|
260
|
+
if i < 0:
|
|
261
|
+
break
|
|
262
|
+
head = i + 8 # start code and the 4 header bytes
|
|
263
|
+
out.write(mm[pos:head])
|
|
264
|
+
out.write(user_data(frames[frame:frame + sizes[k]]))
|
|
265
|
+
frame += sizes[k]
|
|
266
|
+
k += 1
|
|
267
|
+
pos = i = head
|
|
268
|
+
out.write(mm[pos:])
|
|
269
|
+
finally:
|
|
270
|
+
mm.close()
|
|
271
|
+
os.replace(tmp, path)
|
|
272
|
+
return frame
|
dvdcc/errors.py
ADDED
dvdcc/subtitles.py
ADDED
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
"""Subtitle files read as cues: SubRip, WebVTT, ASS / SSA and text scripts
|
|
2
|
+
(number, start and end timecode, text), with drop-frame and non-drop
|
|
3
|
+
timecodes."""
|
|
4
|
+
|
|
5
|
+
import dataclasses
|
|
6
|
+
import os
|
|
7
|
+
import re
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
|
|
10
|
+
from .errors import CaptionError
|
|
11
|
+
|
|
12
|
+
_TIME = re.compile(r"(\d+):(\d{1,2}):(\d{1,2})[,.](\d{1,3})")
|
|
13
|
+
_TAG = re.compile(r"</?[a-zA-Z][^>]*>|\{\\[^}]*\}")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class Cue:
|
|
18
|
+
start: float
|
|
19
|
+
end: float
|
|
20
|
+
lines: list
|
|
21
|
+
position: str = None # "top" | "bottom" | None (the track's / default)
|
|
22
|
+
color: str = None # "#RRGGBB" fill colour from the cue itself, or None
|
|
23
|
+
style_color: str = None # the subtitle file's style colour (after the track's)
|
|
24
|
+
image: str = None # an image script's picture (absolute path): no text
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
_AN = re.compile(r"\\an?(\d)")
|
|
28
|
+
_ASS_COLOR = re.compile(r"\\1?c&H([0-9A-Fa-f]{2,8})&?")
|
|
29
|
+
_FONT_COLOR = re.compile(r'<font[^>]*color\s*=\s*"?#?([0-9A-Fa-f]{6})"?', re.I)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _style_of(text):
|
|
33
|
+
r"""(position, colour) from override tags in a cue's text: ASS-style {\an8}
|
|
34
|
+
(also common in SRT) and {\c&HBBGGRR&}, or SRT <font color="#RRGGBB">."""
|
|
35
|
+
pos = color = None
|
|
36
|
+
for block in re.findall(r"\{([^}]*)\}", text):
|
|
37
|
+
m = _AN.search(block)
|
|
38
|
+
if m:
|
|
39
|
+
n = int(m.group(1))
|
|
40
|
+
# \an7-9 = top (numpad layout); the legacy \a5-7,9-11 also mean top
|
|
41
|
+
pos = "top" if (n in (7, 8, 9) if "an" in block else n >= 5) else "bottom"
|
|
42
|
+
m = _ASS_COLOR.search(block)
|
|
43
|
+
if m:
|
|
44
|
+
bgr = m.group(1)[-6:].rjust(6, "0")
|
|
45
|
+
color = "#" + (bgr[4:6] + bgr[2:4] + bgr[0:2]).upper()
|
|
46
|
+
m = _FONT_COLOR.search(text)
|
|
47
|
+
if m:
|
|
48
|
+
color = "#" + m.group(1).upper()
|
|
49
|
+
return pos, color
|
|
50
|
+
|
|
51
|
+
def _t(m):
|
|
52
|
+
h, mi, s, ms = m.groups()
|
|
53
|
+
return int(h) * 3600 + int(mi) * 60 + int(s) + int(ms.ljust(3, "0")) / 1000
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def parse_srt(text, vtt=False):
|
|
57
|
+
"""Parse SubRip (or, with vtt=True, WebVTT) text into a sorted list of
|
|
58
|
+
non-overlapping Cues."""
|
|
59
|
+
text = text.lstrip("\ufeff").replace("\r\n", "\n").replace("\r", "\n")
|
|
60
|
+
if vtt:
|
|
61
|
+
text = _VTT_TIME.sub(lambda m: "00:" + m.group(0), text) # MM:SS.mmm -> H:MM:SS.mmm
|
|
62
|
+
cues = []
|
|
63
|
+
for block in re.split(r"\n\s*\n", text.strip()):
|
|
64
|
+
lines = block.split("\n")
|
|
65
|
+
idx = next((i for i, l in enumerate(lines) if "-->" in l), None)
|
|
66
|
+
if idx is None:
|
|
67
|
+
continue
|
|
68
|
+
times = _TIME.findall(lines[idx])
|
|
69
|
+
if len(times) < 2:
|
|
70
|
+
raise ValueError(f"bad SRT timing line: {lines[idx]!r}")
|
|
71
|
+
start = _t(_TIME.match(":".join(times[0][:3]) + "," + times[0][3]))
|
|
72
|
+
end = _t(_TIME.match(":".join(times[1][:3]) + "," + times[1][3]))
|
|
73
|
+
raw_body = lines[idx + 1:]
|
|
74
|
+
pos, color = _style_of("\n".join(raw_body))
|
|
75
|
+
if pos is None and vtt:
|
|
76
|
+
m = re.search(r"\bline:(-?\d+(?:\.\d+)?)(%?)", lines[idx])
|
|
77
|
+
if m:
|
|
78
|
+
v = float(m.group(1))
|
|
79
|
+
pos = "top" if (0 <= v < 50 if m.group(2) else 0 <= v < 6) else "bottom"
|
|
80
|
+
body = [_TAG.sub("", l).strip() for l in raw_body]
|
|
81
|
+
body = [l for l in body if l]
|
|
82
|
+
if body and end > start:
|
|
83
|
+
cues.append(Cue(start, end, body, pos, color))
|
|
84
|
+
return _tidy(cues)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _tidy(cues):
|
|
88
|
+
cues.sort(key=lambda c: c.start)
|
|
89
|
+
# a subpicture stream shows one image at a time: clip overlaps
|
|
90
|
+
for a, b in zip(cues, cues[1:]):
|
|
91
|
+
if a.end > b.start:
|
|
92
|
+
a.end = b.start
|
|
93
|
+
return [c for c in cues if c.end - c.start >= 0.04]
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
_VTT_TIME = re.compile(r"(?<![\d:])\d{1,2}:\d{2}\.\d{3}")
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def parse_vtt(text):
|
|
100
|
+
if not text.lstrip("\ufeff").startswith("WEBVTT"):
|
|
101
|
+
raise ValueError("not a WebVTT file (no WEBVTT header)")
|
|
102
|
+
blocks = re.split(r"\n\s*\n", text.replace("\r\n", "\n").replace("\r", "\n"))
|
|
103
|
+
keep = [b for b in blocks if "-->" in b] # drop header, NOTE and STYLE blocks
|
|
104
|
+
return parse_srt("\n\n".join(keep), vtt=True)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def parse_ass(text):
|
|
108
|
+
"""Dialogue lines of an ASS / SSA script (styles: alignment and colour)."""
|
|
109
|
+
text = text.lstrip("\ufeff").replace("\r\n", "\n").replace("\r", "\n")
|
|
110
|
+
styles, fmt, sfmt, cues = {}, None, None, []
|
|
111
|
+
section = ""
|
|
112
|
+
for line in text.split("\n"):
|
|
113
|
+
line = line.strip()
|
|
114
|
+
if line.startswith("["):
|
|
115
|
+
section = line.lower()
|
|
116
|
+
continue
|
|
117
|
+
key, _, val = line.partition(":")
|
|
118
|
+
if "styles" in section and key == "Format":
|
|
119
|
+
sfmt = [x.strip().lower() for x in val.split(",")]
|
|
120
|
+
elif "styles" in section and key == "Style" and sfmt:
|
|
121
|
+
f = dict(zip(sfmt, [x.strip() for x in val.split(",", len(sfmt) - 1)]))
|
|
122
|
+
align = int(f.get("alignment", "2") or 2)
|
|
123
|
+
colour = f.get("primarycolour", "")
|
|
124
|
+
styles[f.get("name", "")] = (
|
|
125
|
+
"top" if align in (7, 8, 9) else None,
|
|
126
|
+
_style_of("{\\c" + colour + "&}")[1] if colour.startswith("&H") else None)
|
|
127
|
+
elif "events" in section and key == "Format":
|
|
128
|
+
fmt = [x.strip().lower() for x in val.split(",")]
|
|
129
|
+
elif "events" in section and key == "Dialogue" and fmt:
|
|
130
|
+
f = dict(zip(fmt, [x.strip() for x in val.split(",", len(fmt) - 1)]))
|
|
131
|
+
try:
|
|
132
|
+
start = _t(_TIME.match(f["start"].replace(".", ",") + "0"))
|
|
133
|
+
end = _t(_TIME.match(f["end"].replace(".", ",") + "0"))
|
|
134
|
+
except (KeyError, AttributeError):
|
|
135
|
+
raise ValueError(f"bad ASS dialogue line: {line[:60]!r}")
|
|
136
|
+
raw = f.get("text", "")
|
|
137
|
+
spos, scol = styles.get(f.get("style", ""), (None, None))
|
|
138
|
+
pos, color = _style_of(raw)
|
|
139
|
+
body = re.sub(r"\{[^}]*\}", "", raw).replace("\\h", " ")
|
|
140
|
+
body = [l.strip() for l in re.split(r"\\[Nn]", body) if l.strip()]
|
|
141
|
+
if body and end > start:
|
|
142
|
+
cues.append(Cue(start, end, body, pos or spos, color, scol))
|
|
143
|
+
if fmt is None:
|
|
144
|
+
raise ValueError("no [Events] Format line: not an ASS / SSA script")
|
|
145
|
+
return _tidy(cues)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
# -- text scripts ---------------------------------------------------------------
|
|
149
|
+
# The plain subtitle script of disc authoring tools: a number, the start and
|
|
150
|
+
# end timecode, the text; lines after it (up to the next numbered line) are
|
|
151
|
+
# more lines of the same subtitle. Spaces or tabs between the fields.
|
|
152
|
+
# 1 00;00;02;02 00;00;03;15 The cat never came back.
|
|
153
|
+
# She just walked away.
|
|
154
|
+
# Timecode: hh;mm;ss;ff is NTSC drop frame; hh:mm:ss:ff counts frames of 30
|
|
155
|
+
# (NTSC, non drop frame) or 25 (PAL); leading zeros may be left out. A time
|
|
156
|
+
# hh:mm:ss,mmm (or .mmm) is read too.
|
|
157
|
+
_TC = TIMECODE = re.compile(r"^(\d{1,2})([:;])(\d{1,2})([:;])(\d{1,2})([:;.,])(\d{1,3})$")
|
|
158
|
+
_SCRIPT_LINE = re.compile(r"^\s*(\d+)[ \t]+(\S+)[ \t]+(\S+)(?:[ \t]+(.*))?$")
|
|
159
|
+
# an image script: the same lines with a picture file instead of the text
|
|
160
|
+
# 1 00;00;02;02 00;00;03;15 sub0001.png
|
|
161
|
+
IMAGE_FILE = re.compile(r"\.(png|bmp|tiff?|jpe?g|webp)$", re.I)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def timecode_seconds(tc, standard="NTSC"):
|
|
165
|
+
"""Seconds from a timecode of the disc's `standard`."""
|
|
166
|
+
m = _TC.match(str(tc).strip())
|
|
167
|
+
if not m:
|
|
168
|
+
raise ValueError(f"{tc!r} is not a timecode (hh;mm;ss;ff or hh:mm:ss:ff)")
|
|
169
|
+
h, mi, s, last, f = int(m[1]), int(m[3]), int(m[5]), m[6], m[7]
|
|
170
|
+
if mi > 59 or s > 59:
|
|
171
|
+
raise ValueError(f"{tc!r}: minutes and seconds go to 59")
|
|
172
|
+
if last in ".,":
|
|
173
|
+
return h * 3600 + mi * 60 + s + int(f.ljust(3, "0")) / 1000
|
|
174
|
+
f = int(f)
|
|
175
|
+
drop = ";" in (m[2], m[4], last)
|
|
176
|
+
base = 30 if drop or standard == "NTSC" else 25
|
|
177
|
+
if f >= base:
|
|
178
|
+
raise ValueError(f"{tc!r}: frame {f} of {base}")
|
|
179
|
+
frames = (h * 3600 + mi * 60 + s) * base + f
|
|
180
|
+
if drop:
|
|
181
|
+
# two frame numbers are left out every minute, except every tenth
|
|
182
|
+
minutes = h * 60 + mi
|
|
183
|
+
frames -= 2 * (minutes - minutes // 10)
|
|
184
|
+
return frames * 1001 / 30000 if base == 30 else frames / 25
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def seconds_timecode(t, standard="NTSC"):
|
|
188
|
+
"""The timecode of a moment: drop frame on NTSC (;), frames of 25 on PAL (:)."""
|
|
189
|
+
if standard != "NTSC":
|
|
190
|
+
frames = int(round(t * 25))
|
|
191
|
+
f, s = frames % 25, frames // 25
|
|
192
|
+
return f"{s // 3600:02d}:{s // 60 % 60:02d}:{s % 60:02d}:{f:02d}"
|
|
193
|
+
frames = int(round(t * 30000 / 1001))
|
|
194
|
+
# (drop frame: 17982 frames per ten minutes, 1798 in each but the first minute)
|
|
195
|
+
tens, rest = divmod(frames, 17982)
|
|
196
|
+
frames += 18 * tens + (2 * ((rest - 2) // 1798) if rest > 1 else 0)
|
|
197
|
+
f, s = frames % 30, frames // 30
|
|
198
|
+
return f"{s // 3600:02d};{s // 60 % 60:02d};{s % 60:02d};{f:02d}"
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def parse_text_script(text, standard="NTSC", base=None):
|
|
202
|
+
"""Cues of a text script. With `base` (the script's folder), a subtitle
|
|
203
|
+
whose text is just a picture file's name (relative to it) is that
|
|
204
|
+
picture: an image script."""
|
|
205
|
+
cues, cur = [], None
|
|
206
|
+
for n, raw in enumerate(text.splitlines(), 1):
|
|
207
|
+
line = raw.strip()
|
|
208
|
+
m = _SCRIPT_LINE.match(line)
|
|
209
|
+
if m and _TC.match(m[2]) and _TC.match(m[3]):
|
|
210
|
+
try:
|
|
211
|
+
start, end = timecode_seconds(m[2], standard), timecode_seconds(m[3], standard)
|
|
212
|
+
except ValueError as e:
|
|
213
|
+
raise ValueError(f"line {n}: {e}")
|
|
214
|
+
if end <= start:
|
|
215
|
+
raise ValueError(f"line {n}: subtitle {m[1]} ends before it starts")
|
|
216
|
+
cur = Cue(start, end, [m[4].strip()] if m[4] and m[4].strip() else [])
|
|
217
|
+
cues.append(cur)
|
|
218
|
+
elif line and cur is not None:
|
|
219
|
+
cur.lines.append(line)
|
|
220
|
+
elif line:
|
|
221
|
+
raise ValueError(f"line {n}: expected \"number start end text\", got {line[:40]!r}")
|
|
222
|
+
if not cues:
|
|
223
|
+
raise ValueError("no subtitles (lines of number, start and end timecode, text)")
|
|
224
|
+
if base is not None:
|
|
225
|
+
for n, c in enumerate(cues, 1):
|
|
226
|
+
if len(c.lines) == 1 and IMAGE_FILE.search(c.lines[0]):
|
|
227
|
+
path = os.path.normpath(os.path.join(base, c.lines[0]))
|
|
228
|
+
if not os.path.isfile(path):
|
|
229
|
+
raise ValueError(f"subtitle {n}: picture {c.lines[0]!r} not found next to "
|
|
230
|
+
"the script")
|
|
231
|
+
c.image, c.lines = path, []
|
|
232
|
+
return _tidy([c for c in cues if c.lines or c.image])
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def to_text_script(cues, standard="NTSC"):
|
|
236
|
+
"""Cues -> a text script (no positions or colours: a script has none)."""
|
|
237
|
+
out = []
|
|
238
|
+
for n, c in enumerate(cues, 1):
|
|
239
|
+
# (a picture: its file name, as an image script has it)
|
|
240
|
+
lines = [os.path.basename(c.image)] if c.image else list(c.lines) or [""]
|
|
241
|
+
out.append(f"{n}\t{seconds_timecode(c.start, standard)}\t"
|
|
242
|
+
f"{seconds_timecode(c.end, standard)}\t{lines[0]}")
|
|
243
|
+
out += lines[1:]
|
|
244
|
+
return "\n".join(out) + "\n"
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def shifted(cues, offset):
|
|
248
|
+
"""Cues `offset` seconds earlier (a script timed against a source's own
|
|
249
|
+
timecode, e.g. one starting at 01;00;00;00); what ends before 0 goes."""
|
|
250
|
+
if not offset:
|
|
251
|
+
return cues
|
|
252
|
+
out = []
|
|
253
|
+
for c in cues:
|
|
254
|
+
if c.end - offset > 0:
|
|
255
|
+
out.append(dataclasses.replace(c, start=max(0.0, c.start - offset),
|
|
256
|
+
end=c.end - offset))
|
|
257
|
+
return out
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
PARSERS = {".srt": parse_srt, ".vtt": parse_vtt, ".ass": parse_ass, ".ssa": parse_ass,
|
|
261
|
+
".txt": parse_text_script}
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def load_subtitles(path, standard="NTSC"):
|
|
265
|
+
"""Cues of a subtitle file (timecodes of a text script are the disc
|
|
266
|
+
`standard`'s)."""
|
|
267
|
+
ext = os.path.splitext(path)[1].lower()
|
|
268
|
+
parse = PARSERS.get(ext, parse_srt)
|
|
269
|
+
if ext == ".txt":
|
|
270
|
+
base = os.path.dirname(os.path.abspath(path))
|
|
271
|
+
parse = lambda text: parse_text_script(text, standard, base) # noqa: E731
|
|
272
|
+
with open(path, "rb") as f:
|
|
273
|
+
raw = f.read()
|
|
274
|
+
for enc in ("utf-8-sig", "cp932", "latin-1"):
|
|
275
|
+
try:
|
|
276
|
+
return parse(raw.decode(enc))
|
|
277
|
+
except UnicodeDecodeError:
|
|
278
|
+
continue
|
|
279
|
+
except ValueError as e:
|
|
280
|
+
raise CaptionError(f"{path}: {e}")
|
|
281
|
+
raise CaptionError(f"{path}: cannot decode subtitle file")
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: dvdcc
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Line-21 (CEA-608) closed captions for DVD-Video: Scenarist SCC or subtitles into MPEG-2 GOP user data
|
|
5
|
+
Author: kajisho5
|
|
6
|
+
License-Expression: Apache-2.0
|
|
7
|
+
Project-URL: Homepage, https://github.com/kajisho5/dvdcc
|
|
8
|
+
Keywords: closed captions,cea-608,line 21,dvd,scc,mpeg-2
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Classifier: Topic :: Multimedia :: Video
|
|
12
|
+
Requires-Python: >=3.11
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
License-File: LICENSE
|
|
15
|
+
License-File: NOTICE
|
|
16
|
+
Dynamic: license-file
|
|
17
|
+
|
|
18
|
+
<p><img src="docs/images/logo/lockup-dark.svg" alt="dvdcc" height="56"></p>
|
|
19
|
+
|
|
20
|
+
# dvdcc
|
|
21
|
+
|
|
22
|
+
Put **line-21 closed captions** (CEA-608, CC1) into the MPEG-2 video of a
|
|
23
|
+
**DVD**, without re-encoding it. Pure Python, no dependencies.
|
|
24
|
+
|
|
25
|
+
* **From a Scenarist (.scc) file:** the byte pairs caption tools write are
|
|
26
|
+
used as they are, from the frame of their timecode.
|
|
27
|
+
* **From subtitles** (SubRip, WebVTT, ASS / SSA, text scripts): each is sent as
|
|
28
|
+
a pop-on caption.
|
|
29
|
+
* It is loaded into the hidden memory before its time, shown at its start,
|
|
30
|
+
and erased at its end.
|
|
31
|
+
* Long lines are wrapped to 32 columns and 4 rows.
|
|
32
|
+
* A caption with too much text to load in time is shown late, with a note.
|
|
33
|
+
* **Into the video:** the captions go into the stream as the DVD's GOP user
|
|
34
|
+
data (`00 00 01 B2 'C' 'C' 01 F8`), one word pair per frame. This is the
|
|
35
|
+
data a DVD player sends to line 21.
|
|
36
|
+
|
|
37
|
+
日本語の説明は [README.ja.md](README.ja.md) にあります。
|
|
38
|
+
|
|
39
|
+
## Why
|
|
40
|
+
|
|
41
|
+
Plenty of tools read DVD captions back, but very few put them in. ffmpeg
|
|
42
|
+
cannot embed .scc files. The open caption libraries that write CEA-608
|
|
43
|
+
target H.264 and HEVC (A/53 SEI), not the DVD's MPEG-2 user data.
|
|
44
|
+
|
|
45
|
+
## Install
|
|
46
|
+
|
|
47
|
+
```sh
|
|
48
|
+
pip install dvdcc
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
A release is made by the version: when the version in `pyproject.toml`
|
|
52
|
+
(and `dvdcc/__init__.py`) reaches `main` without a tag of its own,
|
|
53
|
+
`.github/workflows/release.yml` runs the tests, builds the package, makes
|
|
54
|
+
the GitHub release with its tag and publishes to PyPI (trusted publishing,
|
|
55
|
+
environment `pypi`). Merging the version bump is all it takes.
|
|
56
|
+
|
|
57
|
+
## Use
|
|
58
|
+
|
|
59
|
+
```sh
|
|
60
|
+
dvdcc video.m2v captions.scc # in place
|
|
61
|
+
dvdcc video.m2v subs.srt -o captioned.m2v --offset 01;00;00;00
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
from dvdcc import captions
|
|
66
|
+
|
|
67
|
+
schedule, notes = captions.load("subs.srt") # or a .scc
|
|
68
|
+
frames = captions.inject_file("video.m2v", schedule)
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Then mux and author the disc as usual. Tell the authoring tool that the
|
|
72
|
+
titleset has line-21 captions, so players that look find them. With
|
|
73
|
+
dvdauthor, that is `<video ... caption="field1"/>`.
|
|
74
|
+
|
|
75
|
+
**Limits:**
|
|
76
|
+
|
|
77
|
+
* NTSC only, since line 21 belongs to the 525-line picture.
|
|
78
|
+
* 29.97 frames per second. Film with soft 3:2 pulldown counts its fields
|
|
79
|
+
differently, so it is not supported.
|
|
80
|
+
* Field 1 (CC1) only.
|
|
81
|
+
|
|
82
|
+
## Tests
|
|
83
|
+
|
|
84
|
+
```sh
|
|
85
|
+
python -m unittest discover -s tests -t .
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
The tests cover:
|
|
89
|
+
|
|
90
|
+
* the caption bytes: parity, preamble address codes, the character set;
|
|
91
|
+
* pop-on timing and Scenarist parsing;
|
|
92
|
+
* the user data layout;
|
|
93
|
+
* with ffmpeg, captions put into a test video that **ffmpeg's own caption
|
|
94
|
+
decoder** (lavfi `subcc`) reads back as the text at the right times;
|
|
95
|
+
* with dvdauthor as well, a whole authored disc whose VOB decodes to the
|
|
96
|
+
captions and whose IFO marks line-21 captions.
|
|
97
|
+
|
|
98
|
+
dvdcc was first written for [oncemore](https://github.com/kajisho5/oncemore),
|
|
99
|
+
a DVD and Blu-ray authoring app.
|
|
100
|
+
|
|
101
|
+
## Support
|
|
102
|
+
|
|
103
|
+
dvdcc is free and stays free. If it saves you time, you can support its
|
|
104
|
+
development through [GitHub Sponsors](https://github.com/sponsors/kajisho5)
|
|
105
|
+
(the **Sponsor** button at the top of this repository). Bug reports help just
|
|
106
|
+
as much: [Issues](https://github.com/kajisho5/dvdcc/issues).
|
|
107
|
+
|
|
108
|
+
The logo, colours and writing rules: [docs/DESIGN.md](docs/DESIGN.md).
|
|
109
|
+
|
|
110
|
+
## License
|
|
111
|
+
|
|
112
|
+
Apache License 2.0. See [LICENSE](LICENSE) and [NOTICE](NOTICE).
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
dvdcc/__init__.py,sha256=hlINlBSpyM1aPbxEAoYlt_OkNZTIXeYj2Ckn9kiq27Y,456
|
|
2
|
+
dvdcc/__main__.py,sha256=qcK6EMjC7yWTXGmTL2ogaxNrxdWC9ceg7xEm6yPGaLU,1999
|
|
3
|
+
dvdcc/captions.py,sha256=ThgZ0nOCwlHbZIv7OMUaLg-fswB9bHCwobMcRTGi9eQ,9489
|
|
4
|
+
dvdcc/errors.py,sha256=-zokiF5x2nIAx4_1vLd6nwx9ojD9d1uAgpVd4KRn_s8,232
|
|
5
|
+
dvdcc/subtitles.py,sha256=hzvrwM7lZyNK_e_leNxTiTUrRA0ulf5p6yvvQG24Ssc,11971
|
|
6
|
+
dvdcc-0.1.0.dist-info/licenses/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
|
|
7
|
+
dvdcc-0.1.0.dist-info/licenses/NOTICE,sha256=XiU9Vmb1gZrPLzGZVLwlcO40auoXcm63SAbnReOHK6s,155
|
|
8
|
+
dvdcc-0.1.0.dist-info/METADATA,sha256=IC1ZWT9QlQvpWTA_xaWJbw6J2iHUMumgWyGb11GbpvI,3885
|
|
9
|
+
dvdcc-0.1.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
10
|
+
dvdcc-0.1.0.dist-info/entry_points.txt,sha256=ac3qc88nNtgvoT0OPOJZyq4GClQY3WlfbCQuPTFkUCE,46
|
|
11
|
+
dvdcc-0.1.0.dist-info/top_level.txt,sha256=zXeEowPx8y7aKaMfSKfY_MJZ1UFSsds2Spfg3XfdeIw,6
|
|
12
|
+
dvdcc-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
|
|
2
|
+
Apache License
|
|
3
|
+
Version 2.0, January 2004
|
|
4
|
+
http://www.apache.org/licenses/
|
|
5
|
+
|
|
6
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
7
|
+
|
|
8
|
+
1. Definitions.
|
|
9
|
+
|
|
10
|
+
"License" shall mean the terms and conditions for use, reproduction,
|
|
11
|
+
and distribution as defined by Sections 1 through 9 of this document.
|
|
12
|
+
|
|
13
|
+
"Licensor" shall mean the copyright owner or entity authorized by
|
|
14
|
+
the copyright owner that is granting the License.
|
|
15
|
+
|
|
16
|
+
"Legal Entity" shall mean the union of the acting entity and all
|
|
17
|
+
other entities that control, are controlled by, or are under common
|
|
18
|
+
control with that entity. For the purposes of this definition,
|
|
19
|
+
"control" means (i) the power, direct or indirect, to cause the
|
|
20
|
+
direction or management of such entity, whether by contract or
|
|
21
|
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
22
|
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
23
|
+
|
|
24
|
+
"You" (or "Your") shall mean an individual or Legal Entity
|
|
25
|
+
exercising permissions granted by this License.
|
|
26
|
+
|
|
27
|
+
"Source" form shall mean the preferred form for making modifications,
|
|
28
|
+
including but not limited to software source code, documentation
|
|
29
|
+
source, and configuration files.
|
|
30
|
+
|
|
31
|
+
"Object" form shall mean any form resulting from mechanical
|
|
32
|
+
transformation or translation of a Source form, including but
|
|
33
|
+
not limited to compiled object code, generated documentation,
|
|
34
|
+
and conversions to other media types.
|
|
35
|
+
|
|
36
|
+
"Work" shall mean the work of authorship, whether in Source or
|
|
37
|
+
Object form, made available under the License, as indicated by a
|
|
38
|
+
copyright notice that is included in or attached to the work
|
|
39
|
+
(an example is provided in the Appendix below).
|
|
40
|
+
|
|
41
|
+
"Derivative Works" shall mean any work, whether in Source or Object
|
|
42
|
+
form, that is based on (or derived from) the Work and for which the
|
|
43
|
+
editorial revisions, annotations, elaborations, or other modifications
|
|
44
|
+
represent, as a whole, an original work of authorship. For the purposes
|
|
45
|
+
of this License, Derivative Works shall not include works that remain
|
|
46
|
+
separable from, or merely link (or bind by name) to the interfaces of,
|
|
47
|
+
the Work and Derivative Works thereof.
|
|
48
|
+
|
|
49
|
+
"Contribution" shall mean any work of authorship, including
|
|
50
|
+
the original version of the Work and any modifications or additions
|
|
51
|
+
to that Work or Derivative Works thereof, that is intentionally
|
|
52
|
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
53
|
+
or by an individual or Legal Entity authorized to submit on behalf of
|
|
54
|
+
the copyright owner. For the purposes of this definition, "submitted"
|
|
55
|
+
means any form of electronic, verbal, or written communication sent
|
|
56
|
+
to the Licensor or its representatives, including but not limited to
|
|
57
|
+
communication on electronic mailing lists, source code control systems,
|
|
58
|
+
and issue tracking systems that are managed by, or on behalf of, the
|
|
59
|
+
Licensor for the purpose of discussing and improving the Work, but
|
|
60
|
+
excluding communication that is conspicuously marked or otherwise
|
|
61
|
+
designated in writing by the copyright owner as "Not a Contribution."
|
|
62
|
+
|
|
63
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
64
|
+
on behalf of whom a Contribution has been received by Licensor and
|
|
65
|
+
subsequently incorporated within the Work.
|
|
66
|
+
|
|
67
|
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
68
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
69
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
70
|
+
copyright license to reproduce, prepare Derivative Works of,
|
|
71
|
+
publicly display, publicly perform, sublicense, and distribute the
|
|
72
|
+
Work and such Derivative Works in Source or Object form.
|
|
73
|
+
|
|
74
|
+
3. Grant of Patent License. Subject to the terms and conditions of
|
|
75
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
76
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
77
|
+
(except as stated in this section) patent license to make, have made,
|
|
78
|
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
79
|
+
where such license applies only to those patent claims licensable
|
|
80
|
+
by such Contributor that are necessarily infringed by their
|
|
81
|
+
Contribution(s) alone or by combination of their Contribution(s)
|
|
82
|
+
with the Work to which such Contribution(s) was submitted. If You
|
|
83
|
+
institute patent litigation against any entity (including a
|
|
84
|
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
85
|
+
or a Contribution incorporated within the Work constitutes direct
|
|
86
|
+
or contributory patent infringement, then any patent licenses
|
|
87
|
+
granted to You under this License for that Work shall terminate
|
|
88
|
+
as of the date such litigation is filed.
|
|
89
|
+
|
|
90
|
+
4. Redistribution. You may reproduce and distribute copies of the
|
|
91
|
+
Work or Derivative Works thereof in any medium, with or without
|
|
92
|
+
modifications, and in Source or Object form, provided that You
|
|
93
|
+
meet the following conditions:
|
|
94
|
+
|
|
95
|
+
(a) You must give any other recipients of the Work or
|
|
96
|
+
Derivative Works a copy of this License; and
|
|
97
|
+
|
|
98
|
+
(b) You must cause any modified files to carry prominent notices
|
|
99
|
+
stating that You changed the files; and
|
|
100
|
+
|
|
101
|
+
(c) You must retain, in the Source form of any Derivative Works
|
|
102
|
+
that You distribute, all copyright, patent, trademark, and
|
|
103
|
+
attribution notices from the Source form of the Work,
|
|
104
|
+
excluding those notices that do not pertain to any part of
|
|
105
|
+
the Derivative Works; and
|
|
106
|
+
|
|
107
|
+
(d) If the Work includes a "NOTICE" text file as part of its
|
|
108
|
+
distribution, then any Derivative Works that You distribute must
|
|
109
|
+
include a readable copy of the attribution notices contained
|
|
110
|
+
within such NOTICE file, excluding those notices that do not
|
|
111
|
+
pertain to any part of the Derivative Works, in at least one
|
|
112
|
+
of the following places: within a NOTICE text file distributed
|
|
113
|
+
as part of the Derivative Works; within the Source form or
|
|
114
|
+
documentation, if provided along with the Derivative Works; or,
|
|
115
|
+
within a display generated by the Derivative Works, if and
|
|
116
|
+
wherever such third-party notices normally appear. The contents
|
|
117
|
+
of the NOTICE file are for informational purposes only and
|
|
118
|
+
do not modify the License. You may add Your own attribution
|
|
119
|
+
notices within Derivative Works that You distribute, alongside
|
|
120
|
+
or as an addendum to the NOTICE text from the Work, provided
|
|
121
|
+
that such additional attribution notices cannot be construed
|
|
122
|
+
as modifying the License.
|
|
123
|
+
|
|
124
|
+
You may add Your own copyright statement to Your modifications and
|
|
125
|
+
may provide additional or different license terms and conditions
|
|
126
|
+
for use, reproduction, or distribution of Your modifications, or
|
|
127
|
+
for any such Derivative Works as a whole, provided Your use,
|
|
128
|
+
reproduction, and distribution of the Work otherwise complies with
|
|
129
|
+
the conditions stated in this License.
|
|
130
|
+
|
|
131
|
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
132
|
+
any Contribution intentionally submitted for inclusion in the Work
|
|
133
|
+
by You to the Licensor shall be under the terms and conditions of
|
|
134
|
+
this License, without any additional terms or conditions.
|
|
135
|
+
Notwithstanding the above, nothing herein shall supersede or modify
|
|
136
|
+
the terms of any separate license agreement you may have executed
|
|
137
|
+
with Licensor regarding such Contributions.
|
|
138
|
+
|
|
139
|
+
6. Trademarks. This License does not grant permission to use the trade
|
|
140
|
+
names, trademarks, service marks, or product names of the Licensor,
|
|
141
|
+
except as required for reasonable and customary use in describing the
|
|
142
|
+
origin of the Work and reproducing the content of the NOTICE file.
|
|
143
|
+
|
|
144
|
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
145
|
+
agreed to in writing, Licensor provides the Work (and each
|
|
146
|
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
147
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
148
|
+
implied, including, without limitation, any warranties or conditions
|
|
149
|
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
150
|
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
151
|
+
appropriateness of using or redistributing the Work and assume any
|
|
152
|
+
risks associated with Your exercise of permissions under this License.
|
|
153
|
+
|
|
154
|
+
8. Limitation of Liability. In no event and under no legal theory,
|
|
155
|
+
whether in tort (including negligence), contract, or otherwise,
|
|
156
|
+
unless required by applicable law (such as deliberate and grossly
|
|
157
|
+
negligent acts) or agreed to in writing, shall any Contributor be
|
|
158
|
+
liable to You for damages, including any direct, indirect, special,
|
|
159
|
+
incidental, or consequential damages of any character arising as a
|
|
160
|
+
result of this License or out of the use or inability to use the
|
|
161
|
+
Work (including but not limited to damages for loss of goodwill,
|
|
162
|
+
work stoppage, computer failure or malfunction, or any and all
|
|
163
|
+
other commercial damages or losses), even if such Contributor
|
|
164
|
+
has been advised of the possibility of such damages.
|
|
165
|
+
|
|
166
|
+
9. Accepting Warranty or Additional Liability. While redistributing
|
|
167
|
+
the Work or Derivative Works thereof, You may choose to offer,
|
|
168
|
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
169
|
+
or other liability obligations and/or rights consistent with this
|
|
170
|
+
License. However, in accepting such obligations, You may act only
|
|
171
|
+
on Your own behalf and on Your sole responsibility, not on behalf
|
|
172
|
+
of any other Contributor, and only if You agree to indemnify,
|
|
173
|
+
defend, and hold each Contributor harmless for any liability
|
|
174
|
+
incurred by, or claims asserted against, such Contributor by reason
|
|
175
|
+
of your accepting any such warranty or additional liability.
|
|
176
|
+
|
|
177
|
+
END OF TERMS AND CONDITIONS
|
|
178
|
+
|
|
179
|
+
APPENDIX: How to apply the Apache License to your work.
|
|
180
|
+
|
|
181
|
+
To apply the Apache License to your work, attach the following
|
|
182
|
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
|
183
|
+
replaced with your own identifying information. (Don't include
|
|
184
|
+
the brackets!) The text should be enclosed in the appropriate
|
|
185
|
+
comment syntax for the file format. We also recommend that a
|
|
186
|
+
file or class name and description of purpose be included on the
|
|
187
|
+
same "printed page" as the copyright notice for easier
|
|
188
|
+
identification within third-party archives.
|
|
189
|
+
|
|
190
|
+
Copyright [yyyy] [name of copyright owner]
|
|
191
|
+
|
|
192
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
193
|
+
you may not use this file except in compliance with the License.
|
|
194
|
+
You may obtain a copy of the License at
|
|
195
|
+
|
|
196
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
197
|
+
|
|
198
|
+
Unless required by applicable law or agreed to in writing, software
|
|
199
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
200
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
201
|
+
See the License for the specific language governing permissions and
|
|
202
|
+
limitations under the License.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
dvdcc
|