2013-09-15 09:39:05 +08:00
|
|
|
#!/usr/bin/env python3
|
|
|
|
|
|
|
|
import argparse
|
2013-11-02 21:04:34 +08:00
|
|
|
import calendar
|
2013-09-15 13:11:58 +08:00
|
|
|
import colorsys
|
2013-09-20 17:37:24 +08:00
|
|
|
import gettext
|
2013-11-03 11:59:52 +08:00
|
|
|
import io
|
2013-09-20 16:28:19 +08:00
|
|
|
import json
|
2013-09-15 10:55:36 +08:00
|
|
|
import logging
|
2013-09-15 13:11:58 +08:00
|
|
|
import math
|
2013-10-01 09:07:54 +08:00
|
|
|
import os
|
2013-11-02 20:32:36 +08:00
|
|
|
import random
|
2013-11-03 11:59:52 +08:00
|
|
|
import re
|
2013-09-15 10:55:36 +08:00
|
|
|
import sys
|
2013-11-02 21:04:34 +08:00
|
|
|
import time
|
2013-09-15 10:55:36 +08:00
|
|
|
import xml.dom.minidom
|
|
|
|
|
|
|
|
|
2013-11-02 20:26:01 +08:00
|
|
|
__all__ = ["Danmaku2ASS"]
|
|
|
|
|
2013-11-03 18:20:45 +08:00
|
|
|
if sys.version_info < (3,):
|
|
|
|
raise RuntimeError('at least Python 3.0 is required')
|
2013-11-02 20:26:01 +08:00
|
|
|
|
2013-10-01 09:13:32 +08:00
|
|
|
gettext.install('danmaku2ass', os.path.join(os.path.dirname(os.path.abspath(os.path.realpath(sys.argv[0] or 'locale'))), 'locale'))
|
2013-09-20 17:37:24 +08:00
|
|
|
|
|
|
|
|
2013-11-02 19:58:10 +08:00
|
|
|
def SeekZero(function):
|
|
|
|
def decorated_function(file_):
|
|
|
|
file_.seek(0)
|
|
|
|
try:
|
|
|
|
return function(file_)
|
|
|
|
finally:
|
|
|
|
file_.seek(0)
|
|
|
|
return decorated_function
|
2013-09-15 13:11:58 +08:00
|
|
|
|
2013-09-15 10:55:36 +08:00
|
|
|
|
2013-11-02 21:04:34 +08:00
|
|
|
def EOFAsNone(function):
|
|
|
|
def decorated_function(*args, **kwargs):
|
|
|
|
try:
|
|
|
|
return function(*args, **kwargs)
|
|
|
|
except EOFError:
|
|
|
|
return None
|
|
|
|
return decorated_function
|
|
|
|
|
|
|
|
|
2013-11-02 19:58:10 +08:00
|
|
|
@SeekZero
|
2013-11-02 21:04:34 +08:00
|
|
|
@EOFAsNone
|
2013-09-20 15:00:44 +08:00
|
|
|
def ProbeCommentFormat(f):
|
|
|
|
tmp = f.read(1)
|
2013-09-20 16:28:19 +08:00
|
|
|
if tmp == '[':
|
2013-09-20 15:00:44 +08:00
|
|
|
return 'Acfun'
|
2013-10-06 14:30:26 +08:00
|
|
|
elif tmp == '{':
|
2013-11-02 01:05:19 +08:00
|
|
|
tmp = f.read(14)
|
|
|
|
if tmp == '"status_code":':
|
|
|
|
return 'Tudou'
|
|
|
|
elif tmp == '"root":{"total':
|
2013-10-06 14:30:26 +08:00
|
|
|
return 'sH5V'
|
2013-09-20 15:00:44 +08:00
|
|
|
elif tmp == '<':
|
2013-11-02 13:22:51 +08:00
|
|
|
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'
|
2013-11-02 19:30:45 +08:00
|
|
|
elif tmp == 'xml version="1.0" encoding="utf-8"?><i':
|
|
|
|
return 'Bilibili' # tucao.cc, with the same file format as Bilibili
|
2013-11-02 19:40:30 +08:00
|
|
|
elif tmp == 'xml version="1.0" encoding="Utf-8"?>\n<':
|
|
|
|
return 'Bilibili' # Komica, with the same file format as Bilibili
|
2013-11-02 21:04:34 +08:00
|
|
|
elif tmp == 'xml version="1.0" encoding="UTF-8"?>\n<':
|
|
|
|
return 'MioMio'
|
2013-11-02 13:22:51 +08:00
|
|
|
elif tmp == 'p':
|
|
|
|
return 'Niconico' # Himawari Douga, with the same file format as Niconico Douga
|
2013-09-20 15:00:44 +08:00
|
|
|
|
|
|
|
|
2013-11-02 20:11:38 +08:00
|
|
|
#
|
|
|
|
# ReadComments**** protocol
|
|
|
|
#
|
|
|
|
# Input:
|
|
|
|
# f: Input file
|
|
|
|
# fontsize: Default font size
|
|
|
|
#
|
|
|
|
# Output:
|
|
|
|
# yield a tuple:
|
|
|
|
# (timeline, timestamp, no, comment, pos, color, size, height, width)
|
|
|
|
# timeline: The position when the comment is replayed
|
|
|
|
# timestamp: The UNIX timestamp when the comment is submitted
|
|
|
|
# no: A sequence of 1, 2, 3, ..., used for sorting
|
|
|
|
# comment: The content of the comment
|
|
|
|
# pos: 0 for regular moving comment,
|
|
|
|
# 1 for bottom centered comment,
|
|
|
|
# 2 for top centered comment
|
|
|
|
# color: Font color represented in 0xRRGGBB,
|
|
|
|
# e.g. 0xffffff for white
|
|
|
|
# size: Font size
|
|
|
|
# height: The estimated height in pixels
|
|
|
|
# i.e. (comment.count('\n')+1)*size
|
|
|
|
# width: The estimated width in pixels
|
|
|
|
# i.e. CalculateLength(comment)*size
|
|
|
|
#
|
|
|
|
# After implementing ReadComments****, make sure to update ProbeCommentFormat
|
|
|
|
# and CommentFormatMap.
|
|
|
|
#
|
|
|
|
|
|
|
|
|
2013-09-20 15:00:44 +08:00
|
|
|
def ReadCommentsNiconico(f, fontsize):
|
2013-11-02 20:26:01 +08:00
|
|
|
NiconicoColorMap = {'red': 0xff0000, 'pink': 0xff8080, 'orange': 0xffc000, 'yellow': 0xffff00, 'green': 0x00ff00, 'cyan': 0x00ffff, 'blue': 0x0000ff, 'purple': 0xc000ff, 'black': 0x000000}
|
2013-09-20 15:00:44 +08:00
|
|
|
dom = xml.dom.minidom.parse(f)
|
|
|
|
comment_element = dom.getElementsByTagName('chat')
|
|
|
|
for comment in comment_element:
|
|
|
|
try:
|
|
|
|
c = str(comment.childNodes[0].wholeText)
|
|
|
|
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]
|
2013-09-20 15:08:58 +08:00
|
|
|
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)
|
2013-09-20 15:00:44 +08:00
|
|
|
except (AssertionError, AttributeError, IndexError, TypeError, ValueError):
|
2013-09-20 17:37:24 +08:00
|
|
|
logging.warning(_('Invalid comment: %s') % comment.toxml())
|
2013-09-20 15:00:44 +08:00
|
|
|
continue
|
|
|
|
|
|
|
|
|
2013-09-20 16:28:19 +08:00
|
|
|
def ReadCommentsAcfun(f, fontsize):
|
|
|
|
comment_element = json.load(f)
|
2013-11-03 18:34:09 +08:00
|
|
|
for i, comment in enumerate(comment_element):
|
2013-09-20 16:28:19 +08:00
|
|
|
try:
|
|
|
|
p = str(comment['c']).split(',')
|
|
|
|
assert len(p) >= 6
|
|
|
|
assert p[2] in ('1', '2', '4', '5')
|
|
|
|
c = str(comment['m'])
|
|
|
|
size = int(p[3])*fontsize/25.0
|
|
|
|
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)
|
|
|
|
except (AssertionError, AttributeError, IndexError, TypeError, ValueError):
|
2013-09-20 17:37:24 +08:00
|
|
|
logging.warning(_('Invalid comment: %r') % comment)
|
2013-09-20 16:28:19 +08:00
|
|
|
continue
|
|
|
|
|
|
|
|
|
2013-09-15 10:55:36 +08:00
|
|
|
def ReadCommentsBilibili(f, fontsize):
|
|
|
|
dom = xml.dom.minidom.parse(f)
|
|
|
|
comment_element = dom.getElementsByTagName('d')
|
2013-11-03 18:34:09 +08:00
|
|
|
for i, comment in enumerate(comment_element):
|
2013-09-15 10:55:36 +08:00
|
|
|
try:
|
|
|
|
p = str(comment.getAttribute('p')).split(',')
|
2013-11-02 19:30:45 +08:00
|
|
|
assert len(p) >= 5
|
2013-09-15 10:55:36 +08:00
|
|
|
assert p[1] in ('1', '4', '5')
|
|
|
|
c = str(comment.childNodes[0].wholeText).replace('/n', '\\n')
|
2013-09-15 13:11:58 +08:00
|
|
|
size = int(p[2])*fontsize/25.0
|
2013-09-20 15:00:44 +08:00
|
|
|
yield (float(p[0]), int(p[4]), i, c, {'1': 0, '4': 2, '5': 1}[p[1]], int(p[3]), size, (c.count('\n')+1)*size, CalculateLength(c)*size)
|
2013-09-15 10:55:36 +08:00
|
|
|
except (AssertionError, AttributeError, IndexError, TypeError, ValueError):
|
2013-09-20 17:37:24 +08:00
|
|
|
logging.warning(_('Invalid comment: %s') % comment.toxml())
|
2013-09-15 10:55:36 +08:00
|
|
|
continue
|
|
|
|
|
2013-09-15 09:39:05 +08:00
|
|
|
|
2013-11-02 01:05:19 +08:00
|
|
|
def ReadCommentsTudou(f, fontsize):
|
|
|
|
comment_element = json.load(f)
|
2013-11-03 18:34:09 +08:00
|
|
|
for i, comment in enumerate(comment_element['comment_list']):
|
2013-11-02 01:05:19 +08:00
|
|
|
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
|
|
|
|
|
|
|
|
|
2013-11-02 21:04:34 +08:00
|
|
|
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')
|
2013-11-03 18:34:09 +08:00
|
|
|
for i, comment in enumerate(comment_element):
|
2013-11-02 21:04:34 +08:00
|
|
|
try:
|
|
|
|
message = comment.getElementsByTagName('message')[0]
|
|
|
|
c = str(message.childNodes[0].wholeText)
|
|
|
|
pos = 0
|
|
|
|
size = int(message.getAttribute('fontsize'))*fontsize/25.0
|
2013-11-03 02:21:54 +08:00
|
|
|
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)
|
2013-11-02 21:04:34 +08:00
|
|
|
except (AssertionError, AttributeError, IndexError, TypeError, ValueError):
|
|
|
|
logging.warning(_('Invalid comment: %s') % comment.toxml())
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
2013-10-13 14:56:51 +08:00
|
|
|
def ReadCommentsSH5V(f, fontsize):
|
2013-10-06 14:30:26 +08:00
|
|
|
comment_element = json.load(f)
|
2013-11-03 18:34:09 +08:00
|
|
|
for i, comment in enumerate(comment_element["root"]["bgs"]):
|
2013-10-06 14:30:26 +08:00
|
|
|
try:
|
|
|
|
c_at = str(comment['at'])
|
|
|
|
c_type = str(comment['type'])
|
|
|
|
c_date = str(comment['timestamp'])
|
|
|
|
c_color = str(comment['color'])
|
|
|
|
c = str(comment['text'])
|
|
|
|
size = fontsize
|
2013-10-13 14:56:51 +08:00
|
|
|
yield (float(c_at), int(c_date), i, c, {'0': 0, '1': 0, '4': 2, '5': 1}[c_type], int(c_color[1:], 16), size, (c.count('\n')+1)*size, CalculateLength(c)*size)
|
2013-10-06 14:30:26 +08:00
|
|
|
except (AssertionError, AttributeError, IndexError, TypeError, ValueError):
|
|
|
|
logging.warning(_('Invalid comment: %r') % comment)
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
2013-11-02 21:04:34 +08:00
|
|
|
CommentFormatMap = {None: None, 'Niconico': ReadCommentsNiconico, 'Acfun': ReadCommentsAcfun, 'Bilibili': ReadCommentsBilibili, 'Tudou': ReadCommentsTudou, 'MioMio': ReadCommentsMioMio, 'sH5V': ReadCommentsSH5V}
|
2013-11-02 20:11:38 +08:00
|
|
|
|
|
|
|
|
2013-11-03 18:31:54 +08:00
|
|
|
def ProcessComments(comments, f, width, height, bottomReserved, fontface, fontsize, alpha, lifetime, reduced, progress_callback):
|
2013-11-02 20:32:36 +08:00
|
|
|
styleid = 'Danmaku2ASS_%04x' % random.randint(0, 0xffff)
|
|
|
|
WriteASSHead(f, width, height, fontface, fontsize, alpha, styleid)
|
2013-11-02 19:58:10 +08:00
|
|
|
rows = [[None]*(height-bottomReserved), [None]*(height-bottomReserved), [None]*(height-bottomReserved)]
|
2013-11-03 18:31:54 +08:00
|
|
|
for idx, i in enumerate(comments):
|
|
|
|
if progress_callback and idx%1000 == 0:
|
|
|
|
progress_callback(idx, len(comments))
|
2013-11-02 19:58:10 +08:00
|
|
|
row = 0
|
|
|
|
rowmax = height-bottomReserved-i[7]
|
|
|
|
while row < rowmax:
|
|
|
|
freerows = TestFreeRows(rows, i, row, width, height, bottomReserved, lifetime)
|
|
|
|
if freerows >= i[7]:
|
|
|
|
MarkCommentRow(rows, i, row)
|
2013-11-02 20:32:36 +08:00
|
|
|
WriteComment(f, i, row, width, height, bottomReserved, fontsize, lifetime, styleid)
|
2013-11-02 19:58:10 +08:00
|
|
|
break
|
|
|
|
else:
|
|
|
|
row += freerows or 1
|
|
|
|
else:
|
|
|
|
if not reduced:
|
|
|
|
row = FindAlternativeRow(rows, i, height, bottomReserved)
|
|
|
|
MarkCommentRow(rows, i, row)
|
2013-11-02 20:32:36 +08:00
|
|
|
WriteComment(f, i, row, width, height, bottomReserved, fontsize, lifetime, styleid)
|
2013-11-03 18:31:54 +08:00
|
|
|
if progress_callback:
|
|
|
|
progress_callback(len(comments), len(comments))
|
2013-11-02 19:58:10 +08:00
|
|
|
|
|
|
|
|
|
|
|
def TestFreeRows(rows, c, row, width, height, bottomReserved, lifetime):
|
|
|
|
res = 0
|
|
|
|
rowmax = height-bottomReserved-c[7]
|
|
|
|
while row < rowmax and res < c[7]:
|
|
|
|
if c[4] in (1, 2):
|
|
|
|
if rows[c[4]][row] and rows[c[4]][row][0]+lifetime > c[0]:
|
|
|
|
break
|
|
|
|
else:
|
|
|
|
if rows[c[4]][row] and rows[c[4]][row][0]+lifetime*(rows[c[4]][row][8]+c[8])/width > c[0]:
|
|
|
|
break
|
|
|
|
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
|
2013-09-15 13:11:58 +08:00
|
|
|
|
|
|
|
|
2013-11-02 20:32:36 +08:00
|
|
|
def WriteASSHead(f, width, height, fontface, fontsize, alpha, styleid):
|
2013-09-15 13:11:58 +08:00
|
|
|
f.write(
|
2013-09-19 22:49:41 +08:00
|
|
|
'''\ufeff
|
|
|
|
[Script Info]
|
2013-11-03 00:29:32 +08:00
|
|
|
; Script generated by Danmaku2ASS
|
|
|
|
; https://github.com/m13253/danmaku2ass
|
2013-09-15 13:11:58 +08:00
|
|
|
ScriptType: v4.00+
|
|
|
|
Collisions: Normal
|
|
|
|
PlayResX: %(width)s
|
|
|
|
PlayResY: %(height)s
|
|
|
|
|
|
|
|
[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
|
2013-11-02 20:32:36 +08:00
|
|
|
Style: %(styleid)s, %(fontface)s, %(fontsize)s, &H%(alpha)02XFFFFFF, &H%(alpha)02XFFFFFF, &H%(alpha)02X000000, &H%(alpha)02X000000, 0, 0, 0, 0, 100, 100, 0.00, 0.00, 1, 1, 0, 7, 20, 20, 20, 0
|
2013-09-15 13:11:58 +08:00
|
|
|
|
|
|
|
[Events]
|
|
|
|
Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
|
2013-11-02 20:32:36 +08:00
|
|
|
''' % {'width': width, 'height': height, 'fontface': fontface, 'fontsize': round(fontsize), 'alpha': 255-round(alpha*255), 'styleid': styleid}
|
2013-09-15 13:11:58 +08:00
|
|
|
)
|
|
|
|
|
|
|
|
|
2013-11-02 20:32:36 +08:00
|
|
|
def WriteComment(f, c, row, width, height, bottomReserved, fontsize, lifetime, styleid):
|
2013-11-02 19:58:10 +08:00
|
|
|
text = c[3].replace('\\', '\\\\').replace('\n', '\\N')
|
|
|
|
if c[4] == 1:
|
|
|
|
styles = '{\\an8}{\\pos(%(halfwidth)s, %(row)s)}' % {'halfwidth': round(width/2), 'row': row}
|
|
|
|
elif c[4] == 2:
|
|
|
|
styles = '{\\an2}{\\pos(%(halfwidth)s, %(row)s)}' % {'halfwidth': round(width/2), 'row': ConvertType2(row, height, bottomReserved)}
|
|
|
|
else:
|
|
|
|
styles = '{\\move(%(width)s, %(row)s, %(neglen)s, %(row)s)}' % {'width': width, 'row': row, 'neglen': -math.ceil(c[8])}
|
|
|
|
if not (-1 < c[6]-fontsize < 1):
|
|
|
|
styles += '{\\fs%s}' % round(c[6])
|
|
|
|
if c[5] != 0xffffff:
|
|
|
|
styles += '{\\c&H%02X%02X%02x&}' % (c[5] & 0xff, (c[5] >> 8) & 0xff, (c[5] >> 16) & 0xff)
|
|
|
|
if c[5] == 0x000000:
|
|
|
|
styles += '{\\3c&HFFFFFF&}'
|
2013-11-02 20:32:36 +08:00
|
|
|
f.write('Dialogue: 3,%(start)s,%(end)s,%(styleid)s,,0000,0000,0000,,%(styles)s%(text)s\n' % {'start': ConvertTimestamp(c[0]), 'end': ConvertTimestamp(c[0]+lifetime), 'styles': styles, 'text': text, 'styleid': styleid})
|
2013-09-15 13:11:58 +08:00
|
|
|
|
|
|
|
|
|
|
|
def CalculateLength(s):
|
2013-11-02 20:11:38 +08:00
|
|
|
return max(map(len, s.split('\n'))) # May not be accurate
|
2013-09-15 13:11:58 +08:00
|
|
|
|
|
|
|
|
2013-11-02 19:58:10 +08:00
|
|
|
def ConvertTimestamp(timestamp):
|
|
|
|
hour, minute = divmod(timestamp, 3600)
|
|
|
|
minute, second = divmod(minute, 60)
|
|
|
|
centsecond = round((second-int(second))*100.0)
|
|
|
|
return '%d:%02d:%02d.%02d' % (int(hour), int(minute), int(second), centsecond)
|
|
|
|
|
|
|
|
|
2013-09-15 13:11:58 +08:00
|
|
|
def ConvertType2(row, height, bottomReserved):
|
|
|
|
return height-bottomReserved-row
|
|
|
|
|
|
|
|
|
2013-11-02 19:58:10 +08:00
|
|
|
def NeedWhiteBorder(rgb):
|
|
|
|
h, l, s = colorsys.rgb_to_hls(((rgb >> 16) & 0xff)/255.0, ((rgb >> 8) & 0xff)/255.0, (rgb & 0xff)/255.0)
|
|
|
|
return (1/12 < h < 7/12 and l < 1/3) or l < 5/12
|
|
|
|
|
|
|
|
|
2013-11-02 20:26:01 +08:00
|
|
|
def ConvertToFile(filename_or_file, *args, **kwargs):
|
|
|
|
if isinstance(filename_or_file, str):
|
|
|
|
return open(filename_or_file, *args, **kwargs)
|
|
|
|
else:
|
|
|
|
return filename_or_file
|
|
|
|
|
|
|
|
|
2013-11-03 11:59:52 +08:00
|
|
|
def FilterBadChars(f):
|
|
|
|
s = f.read()
|
2013-11-03 22:58:05 +08:00
|
|
|
s = re.sub('[\\x00-\\x08\\x0b\\x0c\\x0e-\\x1f]', '\ufffd', s)
|
2013-11-03 11:59:52 +08:00
|
|
|
return io.StringIO(s)
|
|
|
|
|
|
|
|
|
2013-11-03 18:31:54 +08:00
|
|
|
def Danmaku2ASS(input_files, output_file, stage_width, stage_height, reserve_blank=0, font_face=_('(FONT) sans-serif')[7:], font_size=25.0, text_opaque=1.0, comment_duration=5.0, is_reduce_comments=False, progress_callback=None):
|
2013-11-03 00:33:06 +08:00
|
|
|
if isinstance(input_files, str):
|
|
|
|
input_files = [input_files]
|
2013-11-02 20:26:01 +08:00
|
|
|
comments = []
|
|
|
|
for i in input_files:
|
|
|
|
with ConvertToFile(i, 'r', encoding='utf-8') as f:
|
|
|
|
CommentProcesser = CommentFormatMap[ProbeCommentFormat(f)]
|
|
|
|
if not CommentProcesser:
|
|
|
|
raise ValueError(_('Unknown comment file format: %s') % i)
|
2013-11-03 11:59:52 +08:00
|
|
|
for comment in CommentProcesser(FilterBadChars(f), font_size):
|
2013-11-02 20:26:01 +08:00
|
|
|
comments.append(comment)
|
|
|
|
try:
|
|
|
|
if output_file:
|
|
|
|
fo = ConvertToFile(output_file, 'w', encoding='utf-8', newline='\r\n')
|
|
|
|
else:
|
|
|
|
fo = sys.stdout
|
|
|
|
comments.sort()
|
2013-11-03 18:31:54 +08:00
|
|
|
ProcessComments(comments, fo, stage_width, stage_height, reserve_blank, font_face, font_size, text_opaque, comment_duration, is_reduce_comments, progress_callback)
|
2013-11-02 20:26:01 +08:00
|
|
|
finally:
|
|
|
|
if output_file:
|
|
|
|
fo.close()
|
|
|
|
|
|
|
|
|
2013-11-02 19:58:10 +08:00
|
|
|
def main():
|
2013-09-15 09:39:05 +08:00
|
|
|
parser = argparse.ArgumentParser()
|
2013-09-30 23:27:33 +08:00
|
|
|
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=_('(FONT) sans-serif')[7:])
|
|
|
|
parser.add_argument('-fs', '--fontsize', metavar=_('SIZE'), help=(_('Default font size')), type=float, default=25.0)
|
|
|
|
parser.add_argument('-a', '--alpha', metavar=_('ALPHA'), help=_('Text opaque'), type=float, default=1.0)
|
|
|
|
parser.add_argument('-l', '--lifetime', metavar=_('SECONDS'), help=_('Duration of comment display'), type=float, default=5.0)
|
|
|
|
parser.add_argument('-p', '--protect', metavar=_('HEIGHT'), help=_('Reserve blank on the bottom of the stage'), type=int, default=0)
|
2013-09-20 17:37:24 +08:00
|
|
|
parser.add_argument('-r', '--reduce', action='store_true', help=_('Reduce the amount of comments if stage is full'))
|
2013-09-30 23:27:33 +08:00
|
|
|
parser.add_argument('file', metavar=_('FILE'), nargs='+', help=_('Comment file to be processed'))
|
2013-09-15 09:39:05 +08:00
|
|
|
args = parser.parse_args()
|
2013-09-15 10:55:36 +08:00
|
|
|
try:
|
|
|
|
width, height = str(args.size).split('x', 1)
|
|
|
|
width = int(width)
|
|
|
|
height = int(height)
|
|
|
|
except ValueError:
|
2013-09-20 17:37:24 +08:00
|
|
|
raise ValueError(_('Invalid stage size: %r') % args.size)
|
2013-11-02 20:26:01 +08:00
|
|
|
Danmaku2ASS(args.file, args.output, width, height, args.protect, args.font, args.fontsize, args.alpha, args.lifetime, args.reduce)
|
2013-11-02 19:58:10 +08:00
|
|
|
|
|
|
|
|
|
|
|
if __name__ == '__main__':
|
|
|
|
main()
|