|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
import io
|
|
import os
|
|
import re
|
|
import sys
|
|
import json
|
|
import math
|
|
import time
|
|
import random
|
|
import gettext
|
|
import logging
|
|
import argparse
|
|
import calendar
|
|
import xml.dom.minidom
|
|
|
|
if sys.version_info < (3,):
|
|
raise RuntimeError("at least Python 3.0 is required")
|
|
|
|
gettext.install(
|
|
"danmaku2ass",
|
|
os.path.join(
|
|
os.path.dirname(os.path.abspath(os.path.realpath(sys.argv[0] or "locale"))),
|
|
"locale",
|
|
),
|
|
)
|
|
|
|
|
|
def SeekZero(function):
|
|
def decorated_function(file_):
|
|
file_.seek(0)
|
|
try:
|
|
return function(file_)
|
|
finally:
|
|
file_.seek(0)
|
|
|
|
return decorated_function
|
|
|
|
|
|
def EOFAsNone(function):
|
|
def decorated_function(*args, **kwargs):
|
|
try:
|
|
return function(*args, **kwargs)
|
|
except EOFError:
|
|
return None
|
|
|
|
return decorated_function
|
|
|
|
|
|
@SeekZero
|
|
@EOFAsNone
|
|
def ProbeCommentFormat(f):
|
|
tmp = f.read(1)
|
|
if tmp == "[":
|
|
return "Acfun"
|
|
|
|
|
|
|
|
elif tmp == "{":
|
|
tmp = f.read(14)
|
|
if tmp == '"status_code":':
|
|
return "Tudou"
|
|
elif tmp.strip().startswith('"result'):
|
|
return "Tudou2"
|
|
elif tmp == "<":
|
|
tmp = f.read(1)
|
|
if tmp == "?":
|
|
tmp = f.read(38)
|
|
if tmp == 'xml version="1.0" encoding="UTF-8"?><p':
|
|
return "Niconico"
|
|
elif tmp == 'xml version="1.0" encoding="UTF-8"?><i':
|
|
return "Bilibili"
|
|
elif tmp == 'xml version="2.0" encoding="UTF-8"?><i':
|
|
return "Bilibili2"
|
|
elif tmp == 'xml version="1.0" encoding="utf-8"?><i':
|
|
return "Bilibili"
|
|
elif tmp == 'xml version="1.0" encoding="Utf-8"?>\n<':
|
|
return "Bilibili"
|
|
elif tmp == 'xml version="1.0" encoding="UTF-8"?>\n<':
|
|
tmp = f.read(20)
|
|
if tmp == "!-- BoonSutazioData=":
|
|
return "Niconico"
|
|
else:
|
|
return "MioMio"
|
|
elif tmp == "p":
|
|
return "Niconico"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def ReadCommentsNiconico(f, fontsize):
|
|
NiconicoColorMap = {
|
|
"red": 0xFF0000,
|
|
"pink": 0xFF8080,
|
|
"orange": 0xFFCC00,
|
|
"yellow": 0xFFFF00,
|
|
"green": 0x00FF00,
|
|
"cyan": 0x00FFFF,
|
|
"blue": 0x0000FF,
|
|
"purple": 0xC000FF,
|
|
"black": 0x000000,
|
|
"niconicowhite": 0xCCCC99,
|
|
"white2": 0xCCCC99,
|
|
"truered": 0xCC0033,
|
|
"red2": 0xCC0033,
|
|
"passionorange": 0xFF6600,
|
|
"orange2": 0xFF6600,
|
|
"madyellow": 0x999900,
|
|
"yellow2": 0x999900,
|
|
"elementalgreen": 0x00CC66,
|
|
"green2": 0x00CC66,
|
|
"marineblue": 0x33FFCC,
|
|
"blue2": 0x33FFCC,
|
|
"nobleviolet": 0x6633CC,
|
|
"purple2": 0x6633CC,
|
|
}
|
|
dom = xml.dom.minidom.parse(f)
|
|
comment_element = dom.getElementsByTagName("chat")
|
|
for comment in comment_element:
|
|
try:
|
|
c = str(comment.childNodes[0].wholeText)
|
|
if c.startswith("/"):
|
|
continue
|
|
pos = 0
|
|
color = 0xFFFFFF
|
|
size = fontsize
|
|
for mailstyle in str(comment.getAttribute("mail")).split():
|
|
if mailstyle == "ue":
|
|
pos = 1
|
|
elif mailstyle == "shita":
|
|
pos = 2
|
|
elif mailstyle == "big":
|
|
size = fontsize * 1.44
|
|
elif mailstyle == "small":
|
|
size = fontsize * 0.64
|
|
elif mailstyle in NiconicoColorMap:
|
|
color = NiconicoColorMap[mailstyle]
|
|
yield (
|
|
max(int(comment.getAttribute("vpos")), 0) * 0.01,
|
|
int(comment.getAttribute("date")),
|
|
int(comment.getAttribute("no")),
|
|
c,
|
|
pos,
|
|
color,
|
|
size,
|
|
(c.count("\n") + 1) * size,
|
|
CalculateLength(c) * size,
|
|
)
|
|
except (AssertionError, AttributeError, IndexError, TypeError, ValueError):
|
|
logging.warning(_("Invalid comment: %s") % comment.toxml())
|
|
continue
|
|
|
|
|
|
def ReadCommentsAcfun(f, fontsize):
|
|
|
|
|
|
|
|
comment_elements = json.load(f)
|
|
comment_element = comment_elements[2]
|
|
for i, comment in enumerate(comment_element):
|
|
try:
|
|
p = str(comment["c"]).split(",")
|
|
assert len(p) >= 6
|
|
assert p[2] in ("1", "2", "4", "5", "7")
|
|
size = int(p[3]) * fontsize / 25.0
|
|
if p[2] != "7":
|
|
c = str(comment["m"]).replace("\\r", "\n").replace("\r", "\n")
|
|
yield (
|
|
float(p[0]),
|
|
int(p[5]),
|
|
i,
|
|
c,
|
|
{"1": 0, "2": 0, "4": 2, "5": 1}[p[2]],
|
|
int(p[1]),
|
|
size,
|
|
(c.count("\n") + 1) * size,
|
|
CalculateLength(c) * size,
|
|
)
|
|
else:
|
|
c = dict(json.loads(comment["m"]))
|
|
yield (float(p[0]), int(p[5]), i, c, "acfunpos", int(p[1]), size, 0, 0)
|
|
except (AssertionError, AttributeError, IndexError, TypeError, ValueError):
|
|
logging.warning(_("Invalid comment: %r") % comment)
|
|
continue
|
|
|
|
|
|
def ReadCommentsBilibili(f, fontsize):
|
|
dom = xml.dom.minidom.parse(f)
|
|
comment_element = dom.getElementsByTagName("d")
|
|
for i, comment in enumerate(comment_element):
|
|
try:
|
|
p = str(comment.getAttribute("p")).split(",")
|
|
assert len(p) >= 5
|
|
assert p[1] in ("1", "4", "5", "6", "7", "8")
|
|
if comment.childNodes.length > 0:
|
|
if p[1] in ("1", "4", "5", "6"):
|
|
c = str(comment.childNodes[0].wholeText).replace("/n", "\n")
|
|
size = int(p[2]) * fontsize / 25.0
|
|
yield (
|
|
float(p[0]),
|
|
int(p[4]),
|
|
i,
|
|
c,
|
|
{"1": 0, "4": 2, "5": 1, "6": 3}[p[1]],
|
|
int(p[3]),
|
|
size,
|
|
(c.count("\n") + 1) * size,
|
|
CalculateLength(c) * size,
|
|
)
|
|
elif p[1] == "7":
|
|
c = str(comment.childNodes[0].wholeText)
|
|
yield (
|
|
float(p[0]),
|
|
int(p[4]),
|
|
i,
|
|
c,
|
|
"bilipos",
|
|
int(p[3]),
|
|
int(p[2]),
|
|
0,
|
|
0,
|
|
)
|
|
elif p[1] == "8":
|
|
pass
|
|
except (AssertionError, AttributeError, IndexError, TypeError, ValueError):
|
|
logging.warning(_("Invalid comment: %s") % comment.toxml())
|
|
continue
|
|
|
|
|
|
def ReadCommentsBilibili2(f, fontsize):
|
|
dom = xml.dom.minidom.parse(f)
|
|
comment_element = dom.getElementsByTagName("d")
|
|
for i, comment in enumerate(comment_element):
|
|
try:
|
|
p = str(comment.getAttribute("p")).split(",")
|
|
assert len(p) >= 7
|
|
assert p[3] in ("1", "4", "5", "6", "7", "8")
|
|
if comment.childNodes.length > 0:
|
|
time = float(p[2]) / 1000.0
|
|
if p[3] in ("1", "4", "5", "6"):
|
|
c = str(comment.childNodes[0].wholeText).replace("/n", "\n")
|
|
size = int(p[4]) * fontsize / 25.0
|
|
yield (
|
|
time,
|
|
int(p[6]),
|
|
i,
|
|
c,
|
|
{"1": 0, "4": 2, "5": 1, "6": 3}[p[3]],
|
|
int(p[5]),
|
|
size,
|
|
(c.count("\n") + 1) * size,
|
|
CalculateLength(c) * size,
|
|
)
|
|
elif p[3] == "7":
|
|
c = str(comment.childNodes[0].wholeText)
|
|
yield (time, int(p[6]), i, c, "bilipos", int(p[5]), int(p[4]), 0, 0)
|
|
elif p[3] == "8":
|
|
pass
|
|
except (AssertionError, AttributeError, IndexError, TypeError, ValueError):
|
|
logging.warning(_("Invalid comment: %s") % comment.toxml())
|
|
continue
|
|
|
|
|
|
def ReadCommentsTudou(f, fontsize):
|
|
comment_element = json.load(f)
|
|
for i, comment in enumerate(comment_element["comment_list"]):
|
|
try:
|
|
assert comment["pos"] in (3, 4, 6)
|
|
c = str(comment["data"])
|
|
assert comment["size"] in (0, 1, 2)
|
|
size = {0: 0.64, 1: 1, 2: 1.44}[comment["size"]] * fontsize
|
|
yield (
|
|
int(comment["replay_time"] * 0.001),
|
|
int(comment["commit_time"]),
|
|
i,
|
|
c,
|
|
{3: 0, 4: 2, 6: 1}[comment["pos"]],
|
|
int(comment["color"]),
|
|
size,
|
|
(c.count("\n") + 1) * size,
|
|
CalculateLength(c) * size,
|
|
)
|
|
except (AssertionError, AttributeError, IndexError, TypeError, ValueError):
|
|
logging.warning(_("Invalid comment: %r") % comment)
|
|
continue
|
|
|
|
|
|
def ReadCommentsTudou2(f, fontsize):
|
|
comment_element = json.load(f)
|
|
for i, comment in enumerate(comment_element["result"]):
|
|
try:
|
|
c = str(comment["content"])
|
|
prop = json.loads(str(comment["propertis"]) or "{}")
|
|
size = int(prop.get("size", 1))
|
|
assert size in (0, 1, 2)
|
|
size = {0: 0.64, 1: 1, 2: 1.44}[size] * fontsize
|
|
pos = int(prop.get("pos", 3))
|
|
assert pos in (0, 3, 4, 6)
|
|
yield (
|
|
int(comment["playat"] * 0.001),
|
|
int(comment["createtime"] * 0.001),
|
|
i,
|
|
c,
|
|
{0: 0, 3: 0, 4: 2, 6: 1}[pos],
|
|
int(prop.get("color", 0xFFFFFF)),
|
|
size,
|
|
(c.count("\n") + 1) * size,
|
|
CalculateLength(c) * size,
|
|
)
|
|
except (AssertionError, AttributeError, IndexError, TypeError, ValueError):
|
|
logging.warning(_("Invalid comment: %r") % comment)
|
|
continue
|
|
|
|
|
|
def ReadCommentsMioMio(f, fontsize):
|
|
NiconicoColorMap = {
|
|
"red": 0xFF0000,
|
|
"pink": 0xFF8080,
|
|
"orange": 0xFFC000,
|
|
"yellow": 0xFFFF00,
|
|
"green": 0x00FF00,
|
|
"cyan": 0x00FFFF,
|
|
"blue": 0x0000FF,
|
|
"purple": 0xC000FF,
|
|
"black": 0x000000,
|
|
}
|
|
dom = xml.dom.minidom.parse(f)
|
|
comment_element = dom.getElementsByTagName("data")
|
|
for i, comment in enumerate(comment_element):
|
|
try:
|
|
message = comment.getElementsByTagName("message")[0]
|
|
c = str(message.childNodes[0].wholeText)
|
|
pos = 0
|
|
size = int(message.getAttribute("fontsize")) * fontsize / 25.0
|
|
yield (
|
|
float(
|
|
comment.getElementsByTagName("playTime")[0].childNodes[0].wholeText
|
|
),
|
|
int(
|
|
calendar.timegm(
|
|
time.strptime(
|
|
comment.getElementsByTagName("times")[0]
|
|
.childNodes[0]
|
|
.wholeText,
|
|
"%Y-%m-%d %H:%M:%S",
|
|
)
|
|
)
|
|
)
|
|
- 28800,
|
|
i,
|
|
c,
|
|
{"1": 0, "4": 2, "5": 1}[message.getAttribute("mode")],
|
|
int(message.getAttribute("color")),
|
|
size,
|
|
(c.count("\n") + 1) * size,
|
|
CalculateLength(c) * size,
|
|
)
|
|
except (AssertionError, AttributeError, IndexError, TypeError, ValueError):
|
|
logging.warning(_("Invalid comment: %s") % comment.toxml())
|
|
continue
|
|
|
|
|
|
CommentFormatMap = {
|
|
"Niconico": ReadCommentsNiconico,
|
|
"Acfun": ReadCommentsAcfun,
|
|
"Bilibili": ReadCommentsBilibili,
|
|
"Bilibili2": ReadCommentsBilibili2,
|
|
"Tudou": ReadCommentsTudou,
|
|
"Tudou2": ReadCommentsTudou2,
|
|
"MioMio": ReadCommentsMioMio,
|
|
}
|
|
|
|
|
|
def WriteCommentBilibiliPositioned(f, c, width, height, styleid):
|
|
|
|
|
|
BiliPlayerSize = (672, 438)
|
|
ZoomFactor = GetZoomFactor(BiliPlayerSize, (width, height))
|
|
|
|
def GetPosition(InputPos, isHeight):
|
|
isHeight = int(isHeight)
|
|
if isinstance(InputPos, int):
|
|
return ZoomFactor[0] * InputPos + ZoomFactor[isHeight + 1]
|
|
elif isinstance(InputPos, float):
|
|
if InputPos > 1:
|
|
return ZoomFactor[0] * InputPos + ZoomFactor[isHeight + 1]
|
|
else:
|
|
return (
|
|
BiliPlayerSize[isHeight] * ZoomFactor[0] * InputPos
|
|
+ ZoomFactor[isHeight + 1]
|
|
)
|
|
else:
|
|
try:
|
|
InputPos = int(InputPos)
|
|
except ValueError:
|
|
InputPos = float(InputPos)
|
|
return GetPosition(InputPos, isHeight)
|
|
|
|
try:
|
|
comment_args = safe_list(json.loads(c[3]))
|
|
text = ASSEscape(str(comment_args[4]).replace("/n", "\n"))
|
|
from_x = comment_args.get(0, 0)
|
|
from_y = comment_args.get(1, 0)
|
|
to_x = comment_args.get(7, from_x)
|
|
to_y = comment_args.get(8, from_y)
|
|
from_x = GetPosition(from_x, False)
|
|
from_y = GetPosition(from_y, True)
|
|
to_x = GetPosition(to_x, False)
|
|
to_y = GetPosition(to_y, True)
|
|
alpha = safe_list(str(comment_args.get(2, "1")).split("-"))
|
|
from_alpha = float(alpha.get(0, 1))
|
|
to_alpha = float(alpha.get(1, from_alpha))
|
|
from_alpha = 255 - round(from_alpha * 255)
|
|
to_alpha = 255 - round(to_alpha * 255)
|
|
rotate_z = int(comment_args.get(5, 0))
|
|
rotate_y = int(comment_args.get(6, 0))
|
|
lifetime = float(comment_args.get(3, 4500))
|
|
duration = int(comment_args.get(9, lifetime * 1000))
|
|
delay = int(comment_args.get(10, 0))
|
|
fontface = comment_args.get(12)
|
|
isborder = comment_args.get(11, "true")
|
|
from_rotarg = ConvertFlashRotation(
|
|
rotate_y, rotate_z, from_x, from_y, width, height
|
|
)
|
|
to_rotarg = ConvertFlashRotation(rotate_y, rotate_z, to_x, to_y, width, height)
|
|
styles = ["\\org(%d, %d)" % (width / 2, height / 2)]
|
|
if from_rotarg[0:2] == to_rotarg[0:2]:
|
|
styles.append("\\pos(%.0f, %.0f)" % (from_rotarg[0:2]))
|
|
else:
|
|
styles.append(
|
|
"\\move(%.0f, %.0f, %.0f, %.0f, %.0f, %.0f)"
|
|
% (from_rotarg[0:2] + to_rotarg[0:2] + (delay, delay + duration))
|
|
)
|
|
styles.append(
|
|
"\\frx%.0f\\fry%.0f\\frz%.0f\\fscx%.0f\\fscy%.0f" % (from_rotarg[2:7])
|
|
)
|
|
if (from_x, from_y) != (to_x, to_y):
|
|
styles.append("\\t(%d, %d, " % (delay, delay + duration))
|
|
styles.append(
|
|
"\\frx%.0f\\fry%.0f\\frz%.0f\\fscx%.0f\\fscy%.0f" % (to_rotarg[2:7])
|
|
)
|
|
styles.append(")")
|
|
if fontface:
|
|
styles.append("\\fn%s" % ASSEscape(fontface))
|
|
styles.append("\\fs%.0f" % (c[6] * ZoomFactor[0]))
|
|
if c[5] != 0xFFFFFF:
|
|
styles.append("\\c&H%s&" % ConvertColor(c[5]))
|
|
if c[5] == 0x000000:
|
|
styles.append("\\3c&HFFFFFF&")
|
|
if from_alpha == to_alpha:
|
|
styles.append("\\alpha&H%02X" % from_alpha)
|
|
elif (from_alpha, to_alpha) == (255, 0):
|
|
styles.append("\\fad(%.0f,0)" % (lifetime * 1000))
|
|
elif (from_alpha, to_alpha) == (0, 255):
|
|
styles.append("\\fad(0, %.0f)" % (lifetime * 1000))
|
|
else:
|
|
styles.append(
|
|
"\\fade(%(from_alpha)d, %(to_alpha)d, %(to_alpha)d, 0, %(end_time).0f, %(end_time).0f, %(end_time).0f)"
|
|
% {
|
|
"from_alpha": from_alpha,
|
|
"to_alpha": to_alpha,
|
|
"end_time": lifetime * 1000,
|
|
}
|
|
)
|
|
if isborder == "false":
|
|
styles.append("\\bord0")
|
|
f.write(
|
|
"Dialogue: -1,%(start)s,%(end)s,%(styleid)s,,0,0,0,,{%(styles)s}%(text)s\n"
|
|
% {
|
|
"start": ConvertTimestamp(c[0]),
|
|
"end": ConvertTimestamp(c[0] + lifetime),
|
|
"styles": "".join(styles),
|
|
"text": text,
|
|
"styleid": styleid,
|
|
}
|
|
)
|
|
except (IndexError, ValueError) as e:
|
|
try:
|
|
logging.warning(_("Invalid comment: %r") % c[3])
|
|
except IndexError:
|
|
logging.warning(_("Invalid comment: %r") % c)
|
|
|
|
|
|
def WriteCommentAcfunPositioned(f, c, width, height, styleid):
|
|
AcfunPlayerSize = (560, 400)
|
|
ZoomFactor = GetZoomFactor(AcfunPlayerSize, (width, height))
|
|
|
|
def GetPosition(InputPos, isHeight):
|
|
isHeight = int(isHeight)
|
|
return (
|
|
AcfunPlayerSize[isHeight] * ZoomFactor[0] * InputPos * 0.001
|
|
+ ZoomFactor[isHeight + 1]
|
|
)
|
|
|
|
def GetTransformStyles(
|
|
x=None,
|
|
y=None,
|
|
scale_x=None,
|
|
scale_y=None,
|
|
rotate_z=None,
|
|
rotate_y=None,
|
|
color=None,
|
|
alpha=None,
|
|
):
|
|
styles = []
|
|
out_x, out_y = x, y
|
|
if rotate_z is not None and rotate_y is not None:
|
|
assert x is not None
|
|
assert y is not None
|
|
rotarg = ConvertFlashRotation(rotate_y, rotate_z, x, y, width, height)
|
|
out_x, out_y = rotarg[0:2]
|
|
if scale_x is None:
|
|
scale_x = 1
|
|
if scale_y is None:
|
|
scale_y = 1
|
|
styles.append(
|
|
"\\frx%.0f\\fry%.0f\\frz%.0f\\fscx%.0f\\fscy%.0f"
|
|
% (rotarg[2:5] + (rotarg[5] * scale_x, rotarg[6] * scale_y))
|
|
)
|
|
else:
|
|
if scale_x is not None:
|
|
styles.append("\\fscx%.0f" % (scale_x * 100))
|
|
if scale_y is not None:
|
|
styles.append("\\fscy%.0f" % (scale_y * 100))
|
|
if color is not None:
|
|
styles.append("\\c&H%s&" % ConvertColor(color))
|
|
if color == 0x000000:
|
|
styles.append("\\3c&HFFFFFF&")
|
|
if alpha is not None:
|
|
alpha = 255 - round(alpha * 255)
|
|
styles.append("\\alpha&H%02X" % alpha)
|
|
return out_x, out_y, styles
|
|
|
|
def FlushCommentLine(f, text, styles, start_time, end_time, styleid):
|
|
if end_time > start_time:
|
|
f.write(
|
|
"Dialogue: -1,%(start)s,%(end)s,%(styleid)s,,0,0,0,,{%(styles)s}%(text)s\n"
|
|
% {
|
|
"start": ConvertTimestamp(start_time),
|
|
"end": ConvertTimestamp(end_time),
|
|
"styles": "".join(styles),
|
|
"text": text,
|
|
"styleid": styleid,
|
|
}
|
|
)
|
|
|
|
try:
|
|
comment_args = c[3]
|
|
text = ASSEscape(str(comment_args["n"]).replace("\r", "\n"))
|
|
common_styles = ["\org(%d, %d)" % (width / 2, height / 2)]
|
|
anchor = {0: 7, 1: 8, 2: 9, 3: 4, 4: 5, 5: 6, 6: 1, 7: 2, 8: 3}.get(
|
|
comment_args.get("c", 0), 7
|
|
)
|
|
if anchor != 7:
|
|
common_styles.append("\\an%s" % anchor)
|
|
font = comment_args.get("w")
|
|
if font:
|
|
font = dict(font)
|
|
fontface = font.get("f")
|
|
if fontface:
|
|
common_styles.append("\\fn%s" % ASSEscape(str(fontface)))
|
|
fontbold = bool(font.get("b"))
|
|
if fontbold:
|
|
common_styles.append("\\b1")
|
|
common_styles.append("\\fs%.0f" % (c[6] * ZoomFactor[0]))
|
|
isborder = bool(comment_args.get("b", True))
|
|
if not isborder:
|
|
common_styles.append("\\bord0")
|
|
to_pos = dict(comment_args.get("p", {"x": 0, "y": 0}))
|
|
to_x = round(GetPosition(int(to_pos.get("x", 0)), False))
|
|
to_y = round(GetPosition(int(to_pos.get("y", 0)), True))
|
|
to_scale_x = float(comment_args.get("e", 1.0))
|
|
to_scale_y = float(comment_args.get("f", 1.0))
|
|
to_rotate_z = float(comment_args.get("r", 0.0))
|
|
to_rotate_y = float(comment_args.get("k", 0.0))
|
|
to_color = c[5]
|
|
to_alpha = float(comment_args.get("a", 1.0))
|
|
from_time = float(comment_args.get("t", 0.0))
|
|
action_time = float(comment_args.get("l", 3.0))
|
|
actions = list(comment_args.get("z", []))
|
|
to_out_x, to_out_y, transform_styles = GetTransformStyles(
|
|
to_x,
|
|
to_y,
|
|
to_scale_x,
|
|
to_scale_y,
|
|
to_rotate_z,
|
|
to_rotate_y,
|
|
to_color,
|
|
to_alpha,
|
|
)
|
|
FlushCommentLine(
|
|
f,
|
|
text,
|
|
common_styles
|
|
+ ["\\pos(%.0f, %.0f)" % (to_out_x, to_out_y)]
|
|
+ transform_styles,
|
|
c[0] + from_time,
|
|
c[0] + from_time + action_time,
|
|
styleid,
|
|
)
|
|
action_styles = transform_styles
|
|
for action in actions:
|
|
action = dict(action)
|
|
from_x, from_y = to_x, to_y
|
|
from_out_x, from_out_y = to_out_x, to_out_y
|
|
from_scale_x, from_scale_y = to_scale_x, to_scale_y
|
|
from_rotate_z, from_rotate_y = to_rotate_z, to_rotate_y
|
|
from_color, from_alpha = to_color, to_alpha
|
|
transform_styles, action_styles = action_styles, []
|
|
from_time += action_time
|
|
action_time = float(action.get("l", 0.0))
|
|
if "x" in action:
|
|
to_x = round(GetPosition(int(action["x"]), False))
|
|
if "y" in action:
|
|
to_y = round(GetPosition(int(action["y"]), True))
|
|
if "f" in action:
|
|
to_scale_x = float(action["f"])
|
|
if "g" in action:
|
|
to_scale_y = float(action["g"])
|
|
if "c" in action:
|
|
to_color = int(action["c"])
|
|
if "t" in action:
|
|
to_alpha = float(action["t"])
|
|
if "d" in action:
|
|
to_rotate_z = float(action["d"])
|
|
if "e" in action:
|
|
to_rotate_y = float(action["e"])
|
|
to_out_x, to_out_y, action_styles = GetTransformStyles(
|
|
to_x,
|
|
to_y,
|
|
from_scale_x,
|
|
from_scale_y,
|
|
to_rotate_z,
|
|
to_rotate_y,
|
|
from_color,
|
|
from_alpha,
|
|
)
|
|
if (from_out_x, from_out_y) == (to_out_x, to_out_y):
|
|
pos_style = "\\pos(%.0f, %.0f)" % (to_out_x, to_out_y)
|
|
else:
|
|
pos_style = "\\move(%.0f, %.0f, %.0f, %.0f)" % (
|
|
from_out_x,
|
|
from_out_y,
|
|
to_out_x,
|
|
to_out_y,
|
|
)
|
|
styles = common_styles + transform_styles
|
|
styles.append(pos_style)
|
|
if action_styles:
|
|
styles.append("\\t(%s)" % ("".join(action_styles)))
|
|
FlushCommentLine(
|
|
f,
|
|
text,
|
|
styles,
|
|
c[0] + from_time,
|
|
c[0] + from_time + action_time,
|
|
styleid,
|
|
)
|
|
except (IndexError, ValueError) as e:
|
|
logging.warning(_("Invalid comment: %r") % c[3])
|
|
|
|
|
|
|
|
|
|
def GetZoomFactor(SourceSize, TargetSize):
|
|
try:
|
|
if (SourceSize, TargetSize) == GetZoomFactor.Cached_Size:
|
|
return GetZoomFactor.Cached_Result
|
|
except AttributeError:
|
|
pass
|
|
GetZoomFactor.Cached_Size = (SourceSize, TargetSize)
|
|
try:
|
|
SourceAspect = SourceSize[0] / SourceSize[1]
|
|
TargetAspect = TargetSize[0] / TargetSize[1]
|
|
if TargetAspect < SourceAspect:
|
|
ScaleFactor = TargetSize[0] / SourceSize[0]
|
|
GetZoomFactor.Cached_Result = (
|
|
ScaleFactor,
|
|
0,
|
|
(TargetSize[1] - TargetSize[0] / SourceAspect) / 2,
|
|
)
|
|
elif TargetAspect > SourceAspect:
|
|
ScaleFactor = TargetSize[1] / SourceSize[1]
|
|
GetZoomFactor.Cached_Result = (
|
|
ScaleFactor,
|
|
(TargetSize[0] - TargetSize[1] * SourceAspect) / 2,
|
|
0,
|
|
)
|
|
else:
|
|
GetZoomFactor.Cached_Result = (TargetSize[0] / SourceSize[0], 0, 0)
|
|
return GetZoomFactor.Cached_Result
|
|
except ZeroDivisionError:
|
|
GetZoomFactor.Cached_Result = (1, 0, 0)
|
|
return GetZoomFactor.Cached_Result
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def ConvertFlashRotation(rotY, rotZ, X, Y, width, height):
|
|
def WrapAngle(deg):
|
|
return 180 - ((180 - deg) % 360)
|
|
|
|
rotY = WrapAngle(rotY)
|
|
rotZ = WrapAngle(rotZ)
|
|
if rotY in (90, -90):
|
|
rotY -= 1
|
|
if rotY == 0 or rotZ == 0:
|
|
outX = 0
|
|
outY = -rotY
|
|
outZ = -rotZ
|
|
rotY *= math.pi / 180.0
|
|
rotZ *= math.pi / 180.0
|
|
else:
|
|
rotY *= math.pi / 180.0
|
|
rotZ *= math.pi / 180.0
|
|
outY = (
|
|
math.atan2(-math.sin(rotY) * math.cos(rotZ), math.cos(rotY)) * 180 / math.pi
|
|
)
|
|
outZ = (
|
|
math.atan2(-math.cos(rotY) * math.sin(rotZ), math.cos(rotZ)) * 180 / math.pi
|
|
)
|
|
outX = math.asin(math.sin(rotY) * math.sin(rotZ)) * 180 / math.pi
|
|
trX = (
|
|
(X * math.cos(rotZ) + Y * math.sin(rotZ)) / math.cos(rotY)
|
|
+ (1 - math.cos(rotZ) / math.cos(rotY)) * width / 2
|
|
- math.sin(rotZ) / math.cos(rotY) * height / 2
|
|
)
|
|
trY = (
|
|
Y * math.cos(rotZ)
|
|
- X * math.sin(rotZ)
|
|
+ math.sin(rotZ) * width / 2
|
|
+ (1 - math.cos(rotZ)) * height / 2
|
|
)
|
|
trZ = (trX - width / 2) * math.sin(rotY)
|
|
FOV = width * math.tan(2 * math.pi / 9.0) / 2
|
|
try:
|
|
scaleXY = FOV / (FOV + trZ)
|
|
except ZeroDivisionError:
|
|
logging.error("Rotation makes object behind the camera: trZ == %.0f" % trZ)
|
|
scaleXY = 1
|
|
trX = (trX - width / 2) * scaleXY + width / 2
|
|
trY = (trY - height / 2) * scaleXY + height / 2
|
|
if scaleXY < 0:
|
|
scaleXY = -scaleXY
|
|
outX += 180
|
|
outY += 180
|
|
logging.error(
|
|
"Rotation makes object behind the camera: trZ == %.0f < %.0f" % (trZ, FOV)
|
|
)
|
|
return (
|
|
trX,
|
|
trY,
|
|
WrapAngle(outX),
|
|
WrapAngle(outY),
|
|
WrapAngle(outZ),
|
|
scaleXY * 100,
|
|
scaleXY * 100,
|
|
)
|
|
|
|
|
|
def ProcessComments(
|
|
comments,
|
|
f,
|
|
width,
|
|
height,
|
|
bottomReserved,
|
|
fontface,
|
|
fontsize,
|
|
alpha,
|
|
duration_marquee,
|
|
duration_still,
|
|
filters_regex,
|
|
reduced,
|
|
progress_callback,
|
|
):
|
|
styleid = "Danmaku2ASS_%04x" % random.randint(0, 0xFFFF)
|
|
WriteASSHead(f, width, height, fontface, fontsize, alpha, styleid)
|
|
rows = [[None] * (height - bottomReserved + 1) for i in range(4)]
|
|
for idx, i in enumerate(comments):
|
|
if progress_callback and idx % 1000 == 0:
|
|
progress_callback(idx, len(comments))
|
|
if isinstance(i[4], int):
|
|
skip = False
|
|
for filter_regex in filters_regex:
|
|
if filter_regex and filter_regex.search(i[3]):
|
|
skip = True
|
|
break
|
|
if skip:
|
|
continue
|
|
row = 0
|
|
rowmax = height - bottomReserved - i[7]
|
|
while row <= rowmax:
|
|
freerows = TestFreeRows(
|
|
rows,
|
|
i,
|
|
row,
|
|
width,
|
|
height,
|
|
bottomReserved,
|
|
duration_marquee,
|
|
duration_still,
|
|
)
|
|
if freerows >= i[7]:
|
|
MarkCommentRow(rows, i, row)
|
|
WriteComment(
|
|
f,
|
|
i,
|
|
row,
|
|
width,
|
|
height,
|
|
bottomReserved,
|
|
fontsize,
|
|
duration_marquee,
|
|
duration_still,
|
|
styleid,
|
|
)
|
|
break
|
|
else:
|
|
row += freerows or 1
|
|
else:
|
|
if not reduced:
|
|
row = FindAlternativeRow(rows, i, height, bottomReserved)
|
|
MarkCommentRow(rows, i, row)
|
|
WriteComment(
|
|
f,
|
|
i,
|
|
row,
|
|
width,
|
|
height,
|
|
bottomReserved,
|
|
fontsize,
|
|
duration_marquee,
|
|
duration_still,
|
|
styleid,
|
|
)
|
|
elif i[4] == "bilipos":
|
|
WriteCommentBilibiliPositioned(f, i, width, height, styleid)
|
|
elif i[4] == "acfunpos":
|
|
WriteCommentAcfunPositioned(f, i, width, height, styleid)
|
|
else:
|
|
logging.warning(_("Invalid comment: %r") % i[3])
|
|
if progress_callback:
|
|
progress_callback(len(comments), len(comments))
|
|
|
|
|
|
def TestFreeRows(
|
|
rows, c, row, width, height, bottomReserved, duration_marquee, duration_still
|
|
):
|
|
res = 0
|
|
rowmax = height - bottomReserved
|
|
targetRow = None
|
|
if c[4] in (1, 2):
|
|
while row < rowmax and res < c[7]:
|
|
if targetRow != rows[c[4]][row]:
|
|
targetRow = rows[c[4]][row]
|
|
if targetRow and targetRow[0] + duration_still > c[0]:
|
|
break
|
|
row += 1
|
|
res += 1
|
|
else:
|
|
try:
|
|
thresholdTime = c[0] - duration_marquee * (1 - width / (c[8] + width))
|
|
except ZeroDivisionError:
|
|
thresholdTime = c[0] - duration_marquee
|
|
while row < rowmax and res < c[7]:
|
|
if targetRow != rows[c[4]][row]:
|
|
targetRow = rows[c[4]][row]
|
|
try:
|
|
if targetRow and (
|
|
targetRow[0] > thresholdTime
|
|
or targetRow[0]
|
|
+ targetRow[8] * duration_marquee / (targetRow[8] + width)
|
|
> c[0]
|
|
):
|
|
break
|
|
except ZeroDivisionError:
|
|
pass
|
|
row += 1
|
|
res += 1
|
|
return res
|
|
|
|
|
|
def FindAlternativeRow(rows, c, height, bottomReserved):
|
|
res = 0
|
|
for row in range(height - bottomReserved - math.ceil(c[7])):
|
|
if not rows[c[4]][row]:
|
|
return row
|
|
elif rows[c[4]][row][0] < rows[c[4]][res][0]:
|
|
res = row
|
|
return res
|
|
|
|
|
|
def MarkCommentRow(rows, c, row):
|
|
try:
|
|
for i in range(row, row + math.ceil(c[7])):
|
|
rows[c[4]][i] = c
|
|
except IndexError:
|
|
pass
|
|
|
|
|
|
def WriteASSHead(f, width, height, fontface, fontsize, alpha, styleid):
|
|
f.write(
|
|
"""[Script Info]
|
|
; Script generated by Danmaku2ASS
|
|
; https://github.com/m13253/danmaku2ass
|
|
Script Updated By: Danmaku2ASS (https://github.com/m13253/danmaku2ass)
|
|
ScriptType: v4.00+
|
|
PlayResX: %(width)d
|
|
PlayResY: %(height)d
|
|
Aspect Ratio: %(width)d:%(height)d
|
|
Collisions: Normal
|
|
WrapStyle: 2
|
|
ScaledBorderAndShadow: yes
|
|
YCbCr Matrix: TV.601
|
|
[V4+ Styles]
|
|
Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
|
|
Style: %(styleid)s, %(fontface)s, %(fontsize).0f, &H%(alpha)02XFFFFFF, &H%(alpha)02XFFFFFF, &H%(alpha)02X000000, &H%(alpha)02X000000, 0, 0, 0, 0, 100, 100, 0.00, 0.00, 1, %(outline).0f, 0, 7, 0, 0, 0, 0
|
|
[Events]
|
|
Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
|
|
"""
|
|
% {
|
|
"width": width,
|
|
"height": height,
|
|
"fontface": fontface,
|
|
"fontsize": fontsize,
|
|
"alpha": 255 - round(alpha * 255),
|
|
"outline": max(fontsize / 25.0, 1),
|
|
"styleid": styleid,
|
|
}
|
|
)
|
|
|
|
|
|
def WriteComment(
|
|
f,
|
|
c,
|
|
row,
|
|
width,
|
|
height,
|
|
bottomReserved,
|
|
fontsize,
|
|
duration_marquee,
|
|
duration_still,
|
|
styleid,
|
|
):
|
|
text = ASSEscape(c[3])
|
|
styles = []
|
|
if c[4] == 1:
|
|
styles.append(
|
|
"\\an8\\pos(%(halfwidth)d, %(row)d)" % {"halfwidth": width / 2, "row": row}
|
|
)
|
|
duration = duration_still
|
|
elif c[4] == 2:
|
|
styles.append(
|
|
"\\an2\\pos(%(halfwidth)d, %(row)d)"
|
|
% {"halfwidth": width / 2, "row": ConvertType2(row, height, bottomReserved)}
|
|
)
|
|
duration = duration_still
|
|
elif c[4] == 3:
|
|
styles.append(
|
|
"\\move(%(neglen)d, %(row)d, %(width)d, %(row)d)"
|
|
% {"width": width, "row": row, "neglen": -math.ceil(c[8])}
|
|
)
|
|
duration = duration_marquee
|
|
else:
|
|
styles.append(
|
|
"\\move(%(width)d, %(row)d, %(neglen)d, %(row)d)"
|
|
% {"width": width, "row": row, "neglen": -math.ceil(c[8])}
|
|
)
|
|
duration = duration_marquee
|
|
if not (-1 < c[6] - fontsize < 1):
|
|
styles.append("\\fs%.0f" % c[6])
|
|
if c[5] != 0xFFFFFF:
|
|
styles.append("\\c&H%s&" % ConvertColor(c[5]))
|
|
if c[5] == 0x000000:
|
|
styles.append("\\3c&HFFFFFF&")
|
|
f.write(
|
|
"Dialogue: 2,%(start)s,%(end)s,%(styleid)s,,0000,0000,0000,,{%(styles)s}%(text)s\n"
|
|
% {
|
|
"start": ConvertTimestamp(c[0]),
|
|
"end": ConvertTimestamp(c[0] + duration),
|
|
"styles": "".join(styles),
|
|
"text": text,
|
|
"styleid": styleid,
|
|
}
|
|
)
|
|
|
|
|
|
def ASSEscape(s):
|
|
def ReplaceLeadingSpace(s):
|
|
sstrip = s.strip(" ")
|
|
slen = len(s)
|
|
if slen == len(sstrip):
|
|
return s
|
|
else:
|
|
llen = slen - len(s.lstrip(" "))
|
|
rlen = slen - len(s.rstrip(" "))
|
|
return "".join(("\u2007" * llen, sstrip, "\u2007" * rlen))
|
|
|
|
return "\\N".join(
|
|
(
|
|
ReplaceLeadingSpace(i) or " "
|
|
for i in str(s)
|
|
.replace("\\", "\\\\")
|
|
.replace("{", "\\{")
|
|
.replace("}", "\\}")
|
|
.split("\n")
|
|
)
|
|
)
|
|
|
|
|
|
def CalculateLength(s):
|
|
return max(map(len, s.split("\n")))
|
|
|
|
|
|
def ConvertTimestamp(timestamp):
|
|
timestamp = round(timestamp * 100.0)
|
|
hour, minute = divmod(timestamp, 360000)
|
|
minute, second = divmod(minute, 6000)
|
|
second, centsecond = divmod(second, 100)
|
|
return "%d:%02d:%02d.%02d" % (int(hour), int(minute), int(second), int(centsecond))
|
|
|
|
|
|
def ConvertColor(RGB, width=1280, height=576):
|
|
if RGB == 0x000000:
|
|
return "000000"
|
|
elif RGB == 0xFFFFFF:
|
|
return "FFFFFF"
|
|
R = (RGB >> 16) & 0xFF
|
|
G = (RGB >> 8) & 0xFF
|
|
B = RGB & 0xFF
|
|
if width < 1280 and height < 576:
|
|
return "%02X%02X%02X" % (B, G, R)
|
|
else:
|
|
ClipByte = lambda x: 255 if x > 255 else 0 if x < 0 else round(x)
|
|
return "%02X%02X%02X" % (
|
|
ClipByte(
|
|
R * 0.00956384088080656
|
|
+ G * 0.03217254540203729
|
|
+ B * 0.95826361371715607
|
|
),
|
|
ClipByte(
|
|
R * -0.10493933142075390
|
|
+ G * 1.17231478191855154
|
|
+ B * -0.06737545049779757
|
|
),
|
|
ClipByte(
|
|
R * 0.91348912373987645
|
|
+ G * 0.07858536372532510
|
|
+ B * 0.00792551253479842
|
|
),
|
|
)
|
|
|
|
|
|
def ConvertType2(row, height, bottomReserved):
|
|
return height - bottomReserved - row
|
|
|
|
|
|
def ConvertToFile(filename_or_file, *args, **kwargs):
|
|
if isinstance(filename_or_file, bytes):
|
|
filename_or_file = str(bytes(filename_or_file).decode("utf-8", "replace"))
|
|
if isinstance(filename_or_file, str):
|
|
return open(filename_or_file, *args, **kwargs)
|
|
else:
|
|
return filename_or_file
|
|
|
|
|
|
def FilterBadChars(f):
|
|
s = f.read()
|
|
s = re.sub("[\\x00-\\x08\\x0b\\x0c\\x0e-\\x1f]", "\ufffd", s)
|
|
return io.StringIO(s)
|
|
|
|
|
|
class safe_list(list):
|
|
def get(self, index, default=None):
|
|
try:
|
|
return self[index]
|
|
except IndexError:
|
|
return default
|
|
|
|
|
|
def export(func):
|
|
global __all__
|
|
try:
|
|
__all__.append(func.__name__)
|
|
except NameError:
|
|
__all__ = [func.__name__]
|
|
return func
|
|
|
|
|
|
@export
|
|
def Danmaku2ASS(
|
|
input_files,
|
|
input_format,
|
|
output_file,
|
|
stage_width,
|
|
stage_height,
|
|
reserve_blank=0,
|
|
font_face=_("(FONT) sans-serif")[7:],
|
|
font_size=25.0,
|
|
text_opacity=1.0,
|
|
duration_marquee=5.0,
|
|
duration_still=5.0,
|
|
comment_filter=None,
|
|
comment_filters_file=None,
|
|
is_reduce_comments=False,
|
|
progress_callback=None,
|
|
):
|
|
comment_filters = [comment_filter]
|
|
if comment_filters_file:
|
|
with open(comment_filters_file, "r") as f:
|
|
d = f.readlines()
|
|
comment_filters.extend([i.strip() for i in d])
|
|
filters_regex = []
|
|
for comment_filter in comment_filters:
|
|
try:
|
|
if comment_filter:
|
|
filters_regex.append(re.compile(comment_filter))
|
|
except:
|
|
raise ValueError(_("Invalid regular expression: %s") % comment_filter)
|
|
fo = None
|
|
comments = ReadComments(input_files, input_format, font_size)
|
|
try:
|
|
if output_file:
|
|
fo = ConvertToFile(
|
|
output_file, "w", encoding="utf-8-sig", errors="replace", newline="\r\n"
|
|
)
|
|
else:
|
|
fo = sys.stdout
|
|
ProcessComments(
|
|
comments,
|
|
fo,
|
|
stage_width,
|
|
stage_height,
|
|
reserve_blank,
|
|
font_face,
|
|
font_size,
|
|
text_opacity,
|
|
duration_marquee,
|
|
duration_still,
|
|
filters_regex,
|
|
is_reduce_comments,
|
|
progress_callback,
|
|
)
|
|
finally:
|
|
if output_file and fo != output_file:
|
|
fo.close()
|
|
|
|
|
|
@export
|
|
def ReadComments(input_files, input_format, font_size=25.0, progress_callback=None):
|
|
if isinstance(input_files, bytes):
|
|
input_files = str(bytes(input_files).decode("utf-8", "replace"))
|
|
if isinstance(input_files, str):
|
|
input_files = [input_files]
|
|
else:
|
|
input_files = list(input_files)
|
|
comments = []
|
|
for idx, i in enumerate(input_files):
|
|
if progress_callback:
|
|
progress_callback(idx, len(input_files))
|
|
with ConvertToFile(i, "r", encoding="utf-8", errors="replace") as f:
|
|
s = f.read()
|
|
str_io = io.StringIO(s)
|
|
if input_format == "autodetect":
|
|
CommentProcessor = GetCommentProcessor(str_io)
|
|
if not CommentProcessor:
|
|
raise ValueError(_("Failed to detect comment file format: %s") % i)
|
|
else:
|
|
CommentProcessor = CommentFormatMap.get(input_format)
|
|
if not CommentProcessor:
|
|
raise ValueError(
|
|
_("Unknown comment file format: %s") % input_format
|
|
)
|
|
comments.extend(CommentProcessor(FilterBadChars(str_io), font_size))
|
|
if progress_callback:
|
|
progress_callback(len(input_files), len(input_files))
|
|
comments.sort()
|
|
return comments
|
|
|
|
|
|
@export
|
|
def GetCommentProcessor(input_file):
|
|
return CommentFormatMap.get(ProbeCommentFormat(input_file))
|
|
|
|
|
|
def main():
|
|
logging.basicConfig(format="%(levelname)s: %(message)s")
|
|
if len(sys.argv) == 1:
|
|
sys.argv.append("--help")
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument(
|
|
"-f",
|
|
"--format",
|
|
metavar=_("FORMAT"),
|
|
help=_("Format of input file (autodetect|%s) [default: autodetect]")
|
|
% "|".join(i for i in CommentFormatMap),
|
|
default="autodetect",
|
|
)
|
|
parser.add_argument("-o", "--output", metavar=_("OUTPUT"), help=_("Output file"))
|
|
parser.add_argument(
|
|
"-s",
|
|
"--size",
|
|
metavar=_("WIDTHxHEIGHT"),
|
|
required=True,
|
|
help=_("Stage size in pixels"),
|
|
)
|
|
parser.add_argument(
|
|
"-fn",
|
|
"--font",
|
|
metavar=_("FONT"),
|
|
help=_("Specify font face [default: %s]") % _("(FONT) sans-serif")[7:],
|
|
default=_("(FONT) sans-serif")[7:],
|
|
)
|
|
parser.add_argument(
|
|
"-fs",
|
|
"--fontsize",
|
|
metavar=_("SIZE"),
|
|
help=(_("Default font size [default: %s]") % 25),
|
|
type=float,
|
|
default=25.0,
|
|
)
|
|
parser.add_argument(
|
|
"-a",
|
|
"--alpha",
|
|
metavar=_("ALPHA"),
|
|
help=_("Text opacity"),
|
|
type=float,
|
|
default=1.0,
|
|
)
|
|
parser.add_argument(
|
|
"-dm",
|
|
"--duration-marquee",
|
|
metavar=_("SECONDS"),
|
|
help=_("Duration of scrolling comment display [default: %s]") % 5,
|
|
type=float,
|
|
default=5.0,
|
|
)
|
|
parser.add_argument(
|
|
"-ds",
|
|
"--duration-still",
|
|
metavar=_("SECONDS"),
|
|
help=_("Duration of still comment display [default: %s]") % 5,
|
|
type=float,
|
|
default=5.0,
|
|
)
|
|
parser.add_argument(
|
|
"-fl", "--filter", help=_("Regular expression to filter comments")
|
|
)
|
|
parser.add_argument(
|
|
"-flf",
|
|
"--filter-file",
|
|
help=_("Regular expressions from file (one line one regex) to filter comments"),
|
|
)
|
|
parser.add_argument(
|
|
"-p",
|
|
"--protect",
|
|
metavar=_("HEIGHT"),
|
|
help=_("Reserve blank on the bottom of the stage"),
|
|
type=int,
|
|
default=0,
|
|
)
|
|
parser.add_argument(
|
|
"-r",
|
|
"--reduce",
|
|
action="store_true",
|
|
help=_("Reduce the amount of comments if stage is full"),
|
|
)
|
|
parser.add_argument(
|
|
"file", metavar=_("FILE"), nargs="+", help=_("Comment file to be processed")
|
|
)
|
|
args = parser.parse_args()
|
|
try:
|
|
width, height = str(args.size).split("x", 1)
|
|
width = int(width)
|
|
height = int(height)
|
|
except ValueError:
|
|
raise ValueError(_("Invalid stage size: %r") % args.size)
|
|
Danmaku2ASS(
|
|
args.file,
|
|
args.format,
|
|
args.output,
|
|
width,
|
|
height,
|
|
args.protect,
|
|
args.font,
|
|
args.fontsize,
|
|
args.alpha,
|
|
args.duration_marquee,
|
|
args.duration_still,
|
|
args.filter,
|
|
args.filter_file,
|
|
args.reduce,
|
|
)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|
|
|