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-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-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,
2013-11-17 13:52:46 +08:00
# 2 for top centered comment,
# 3 for reversed moving comment
2013-11-02 20:11:38 +08:00
# 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-16 22:48:06 +08:00
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 }
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 )
2013-11-16 23:14:24 +08:00
if c . startswith ( ' / ' ) :
continue # ignore advanced comments
2013-09-20 15:00:44 +08:00
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
2013-12-07 23:38:24 +08:00
assert p [ 2 ] in ( ' 1 ' , ' 2 ' , ' 4 ' , ' 5 ' , ' 7 ' )
2013-09-20 16:28:19 +08:00
c = str ( comment [ ' m ' ] )
size = int ( p [ 3 ] ) * fontsize / 25.0
2013-12-07 23:38:24 +08:00
if p [ 2 ] != ' 7 ' :
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 ( c ) )
yield ( float ( p [ 0 ] ) , int ( p [ 5 ] ) , i , c , ' acfunpos ' , int ( p [ 1 ] ) , size , 0 , 0 )
2013-09-20 16:28:19 +08:00
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-11-16 22:17:48 +08:00
assert p [ 1 ] in ( ' 1 ' , ' 4 ' , ' 5 ' , ' 6 ' , ' 7 ' )
2013-11-16 13:51:32 +08:00
if p [ 1 ] != ' 7 ' :
c = str ( comment . childNodes [ 0 ] . wholeText ) . replace ( ' /n ' , ' \n ' )
size = int ( p [ 2 ] ) * fontsize / 25.0
2013-11-17 13:52:46 +08:00
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 )
2013-11-16 13:51:32 +08:00
else : # positioned comment
c = str ( comment . childNodes [ 0 ] . wholeText )
yield ( float ( p [ 0 ] ) , int ( p [ 4 ] ) , i , c , ' bilipos ' , int ( p [ 3 ] ) , int ( p [ 2 ] ) , 0 , 0 )
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-16 13:51:32 +08:00
def WriteCommentBilibiliPositioned ( f , c , width , height , styleid ) :
2013-11-17 14:00:45 +08:00
BiliPlayerSize = ( 672 , 438 )
2013-12-07 18:27:17 +08:00
ZoomFactor = GetZoomFactor ( BiliPlayerSize , ( width , height ) )
2013-11-17 09:10:12 +08:00
2013-11-16 22:06:10 +08:00
def GetPosition ( InputPos , isHeight ) :
isHeight = int ( isHeight ) # True -> 1
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 )
2013-11-17 09:10:12 +08:00
2013-11-16 13:51:32 +08:00
try :
comment_args = safe_list ( json . loads ( c [ 3 ] ) )
2013-11-17 14:59:25 +08:00
text = ASSEscape ( str ( comment_args [ 4 ] ) . replace ( ' /n ' , ' \n ' ) )
2013-11-16 22:06:10 +08:00
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 = round ( GetPosition ( from_x , False ) )
from_y = round ( GetPosition ( from_y , True ) )
to_x = round ( GetPosition ( to_x , False ) )
to_y = round ( GetPosition ( to_y , True ) )
alpha = safe_list ( str ( comment_args . get ( 2 , ' 1 ' ) ) . split ( ' - ' ) )
2013-11-16 13:51:32 +08:00
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 )
2013-11-16 22:06:10 +08:00
rotate_z = - int ( comment_args . get ( 5 , 0 ) )
2013-11-17 13:48:06 +08:00
rotate_y = - int ( comment_args . get ( 6 , 0 ) )
2013-11-16 13:51:32 +08:00
lifetime = float ( comment_args . get ( 3 , 4500 ) )
2013-11-16 22:06:10 +08:00
duration = int ( comment_args . get ( 9 , lifetime * 1000 ) )
delay = int ( comment_args . get ( 10 , 0 ) )
2013-11-16 13:51:32 +08:00
fontface = comment_args . get ( 12 )
isborder = comment_args . get ( 11 , ' true ' )
styles = [ ]
2013-11-16 22:06:10 +08:00
if ( from_x , from_y ) == ( to_x , to_y ) :
2013-11-16 22:37:58 +08:00
styles . append ( ' \\ pos( %s , %s ) ' % ( from_x , from_y ) )
2013-11-16 22:06:10 +08:00
else :
2013-11-16 22:37:58 +08:00
styles . append ( ' \\ move( %s , %s , %s , %s , %s , %s ) ' % ( from_x , from_y , to_x , to_y , delay , delay + duration ) )
2013-11-16 22:06:10 +08:00
if rotate_z != 0 :
2013-11-16 22:37:58 +08:00
styles . append ( ' \\ frz %s ' % rotate_z )
2013-11-24 12:59:09 +08:00
if rotate_y != 0 :
styles . append ( ' \\ frx %s ' % ( rotate_y * math . sin ( rotate_z * math . pi / 180 ) ) )
styles . append ( ' \\ fry %s ' % ( rotate_y * math . cos ( rotate_z * math . pi / 180 ) ) )
elif rotate_y != 0 :
2013-11-16 22:37:58 +08:00
styles . append ( ' \\ fry %s ' % rotate_y )
2013-11-16 13:51:32 +08:00
if fontface :
2013-12-07 23:38:24 +08:00
styles . append ( ' \\ fn %s ' % ASSEscape ( fontface ) )
2013-11-16 22:37:58 +08:00
styles . append ( ' \\ fs %s ' % round ( c [ 6 ] * ZoomFactor [ 0 ] ) )
2013-11-16 13:51:32 +08:00
if c [ 5 ] != 0xffffff :
2013-11-16 22:37:58 +08:00
styles . append ( ' \\ c&H %02X %02X %02x & ' % ( c [ 5 ] & 0xff , ( c [ 5 ] >> 8 ) & 0xff , ( c [ 5 ] >> 16 ) & 0xff ) )
2013-11-16 13:51:32 +08:00
if c [ 5 ] == 0x000000 :
2013-11-16 22:37:58 +08:00
styles . append ( ' \\ 3c&HFFFFFF& ' )
2013-11-16 22:06:10 +08:00
if from_alpha == to_alpha :
2013-11-16 22:37:58 +08:00
styles . append ( ' \\ alpha&H %02X ' % from_alpha )
2013-11-24 13:04:44 +08:00
elif ( from_alpha , to_alpha ) == ( 255 , 0 ) :
styles . append ( ' \\ fad( %s ,0) ' % ( lifetime * 1000 ) )
elif ( from_alpha , to_alpha ) == ( 0 , 255 ) :
styles . append ( ' \\ fad(0, %s ) ' % ( lifetime * 1000 ) )
2013-11-16 22:06:10 +08:00
else :
2013-11-24 14:08:22 +08:00
styles . append ( ' \\ fade( %(from_alpha)s , %(to_alpha)s , %(to_alpha)s , 0, %(end_time)s , %(end_time)s , %(end_time)s ) ' % { ' from_alpha ' : from_alpha , ' to_alpha ' : to_alpha , ' end_time ' : lifetime * 1000 } )
2013-11-16 13:51:32 +08:00
if isborder == ' false ' :
2013-11-16 22:37:58 +08:00
styles . append ( ' \\ bord0 ' )
2013-12-01 16:56:42 +08:00
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 } )
2013-12-07 23:38:24 +08:00
except ( IndexError , ValueError ) as e :
2013-11-16 13:51:32 +08:00
try :
logging . warning ( _ ( ' Invalid comment: %r ' ) % c [ 3 ] )
except IndexError :
logging . warning ( _ ( ' Invalid comment: %r ' ) % c )
2013-12-07 23:38:24 +08:00
def WriteCommentAcfunPositioned ( f , c , width , height , styleid ) :
AcfunPlayerSize = ( 650 , 445 )
ZoomFactor = GetZoomFactor ( AcfunPlayerSize , ( width , height ) )
def GetPosition ( InputPos , isHeight ) :
isHeight = int ( isHeight ) # True -> 1
return AcfunPlayerSize [ isHeight ] * ZoomFactor [ 0 ] * InputPos * 0.001 + ZoomFactor [ isHeight + 1 ]
try :
comment_args = c [ 3 ]
text = str ( comment_args [ ' n ' ] )
styles = [ ]
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 :
styles . append ( ' \\ an %s ' % anchor )
from_pos = dict ( comment_args . get ( ' p ' , { ' x ' : 0 , ' y ' : 0 } ) )
from_x = round ( GetPosition ( int ( from_pos . get ( ' x ' , 0 ) ) , False ) )
from_y = round ( GetPosition ( int ( from_pos . get ( ' y ' , 0 ) ) , True ) )
styles . append ( ' \\ pos( %s , %s ) ' % ( from_x , from_y ) )
styles . append ( ' \\ fs %s ' % round ( c [ 6 ] * ZoomFactor [ 0 ] ) )
scale_x = round ( float ( comment_args . get ( ' e ' , 1.0 ) ) * 100 )
if scale_x != 100 :
styles . append ( ' \\ fscx %s ' % scale_x )
scale_y = round ( float ( comment_args . get ( ' f ' , 1.0 ) ) * 100 )
if scale_y != 100 :
styles . append ( ' \\ fscy %s ' % scale_y )
rotate_z = - float ( comment_args . get ( ' r ' , 0.0 ) )
rotate_y = - float ( comment_args . get ( ' k ' , 0.0 ) )
if not ( - 1 < rotate_z < 1 ) :
styles . append ( ' \\ frz %s ' % round ( rotate_z ) )
if not ( - 1 < rotate_y < 1 ) :
styles . append ( ' \\ frx %s ' % round ( rotate_y * math . sin ( rotate_z * math . pi / 180 ) ) )
styles . append ( ' \\ fry %s ' % round ( rotate_y * math . cos ( rotate_z * math . pi / 180 ) ) )
elif not ( - 1 < rotate_y < 1 ) :
styles . append ( ' \\ fry %s ' % round ( rotate_y ) )
font = comment_args . get ( ' w ' )
if font :
font = dict ( font )
fontface = font . get ( ' f ' )
if fontface :
styles . append ( ' \\ fn %s ' % ASSEscape ( str ( fontface ) ) )
fontbold = bool ( font . get ( ' b ' ) )
if fontbold :
styles . append ( ' \\ b1 ' )
if c [ 5 ] != 0xffffff :
styles . append ( ' \\ c&H %02X %02X %02x & ' % ( c [ 5 ] & 0xff , ( c [ 5 ] >> 8 ) & 0xff , ( c [ 5 ] >> 16 ) & 0xff ) )
if c [ 5 ] == 0x000000 :
styles . append ( ' \\ 3c&HFFFFFF& ' )
from_alpha = 255 - round ( float ( comment_args . get ( ' a ' , 1.0 ) ) * 255 )
styles . append ( ' \\ alpha&H %02X ' % from_alpha )
isborder = bool ( comment_args . get ( ' b ' , True ) )
if not isborder :
styles . append ( ' \\ bord0 ' )
appear_time = float ( comment_args . get ( ' t ' , 0.0 ) )
action_time = float ( comment_args . get ( ' l ' , 3.0 ) )
actions = list ( comment_args . get ( ' z ' , [ ] ) )
to_x , to_y = from_x , from_y
for action in actions :
action = dict ( action )
duration = float ( action . get ( ' l ' , 0.0 ) )
if duration < = 0.0 :
continue
action_styles = [ ]
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 ( ' x ' in action ) or ( ' y ' in action ) :
action_styles . append ( ' \\ pos( %s , %s ) ' % ( to_x , to_y ) )
if ' f ' in action :
action_styles . append ( ' \\ fscx %s ' % round ( float ( action [ ' f ' ] ) * 100 ) )
if ' g ' in action :
action_styles . append ( ' \\ fscy %s ' % round ( float ( action [ ' g ' ] ) * 100 ) )
if action_styles :
styles . append ( ' \\ t( %s , %s , %s ) ' % ( round ( action_time * 1000 ) , round ( ( action_time + duration ) * 1000 ) , ' ' . join ( action_styles ) ) )
action_time + = duration
f . write ( ' Dialogue: -1, %(start)s , %(end)s , %(styleid)s ,,0,0,0,, { %(styles)s } %(text)s \n ' % { ' start ' : ConvertTimestamp ( c [ 0 ] + appear_time ) , ' end ' : ConvertTimestamp ( c [ 0 ] + appear_time + action_time ) , ' styles ' : ' ' . join ( styles ) , ' text ' : text , ' styleid ' : styleid } )
except ( IndexError , ValueError ) as e :
logging . warning ( _ ( ' Invalid comment: %r ' ) % c [ 3 ] )
2013-12-07 18:27:17 +08:00
# Result: (f, dx, dy)
# To convert: NewX = f*x+dx, NewY = f*y+dy
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 : # narrower
ScaleFactor = TargetSize [ 0 ] / SourceSize [ 0 ]
GetZoomFactor . Cached_Result = ( ScaleFactor , 0 , ( TargetSize [ 1 ] - TargetSize [ 0 ] / SourceAspect ) / 2 )
elif TargetAspect > SourceAspect : # wider
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
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-12-01 16:48:28 +08:00
rows = [ [ None ] * ( height - bottomReserved + 1 ) for i in range ( 4 ) ]
2013-11-03 18:31:54 +08:00
for idx , i in enumerate ( comments ) :
2013-11-08 19:47:11 +08:00
if progress_callback and idx % 1000 == 0 :
2013-11-03 18:31:54 +08:00
progress_callback ( idx , len ( comments ) )
2013-11-16 13:51:32 +08:00
if isinstance ( i [ 4 ] , int ) :
row = 0
rowmax = height - bottomReserved - i [ 7 ]
2013-12-01 16:48:28 +08:00
while row < = rowmax :
2013-11-16 13:51:32 +08:00
freerows = TestFreeRows ( rows , i , row , width , height , bottomReserved , lifetime )
if freerows > = i [ 7 ] :
MarkCommentRow ( rows , i , row )
WriteComment ( f , i , row , width , height , bottomReserved , fontsize , lifetime , styleid )
break
else :
row + = freerows or 1
2013-11-02 19:58:10 +08:00
else :
2013-11-16 13:51:32 +08:00
if not reduced :
row = FindAlternativeRow ( rows , i , height , bottomReserved )
MarkCommentRow ( rows , i , row )
WriteComment ( f , i , row , width , height , bottomReserved , fontsize , lifetime , styleid )
elif i [ 4 ] == ' bilipos ' :
WriteCommentBilibiliPositioned ( f , i , width , height , styleid )
2013-12-07 23:38:24 +08:00
elif i [ 4 ] == ' acfunpos ' :
WriteCommentAcfunPositioned ( f , i , width , height , styleid )
2013-11-02 19:58:10 +08:00
else :
2013-11-16 13:51:32 +08:00
logging . warning ( _ ( ' Invalid comment: %r ' ) % i [ 3 ] )
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
2013-12-02 22:38:44 +08:00
rowmax = height - bottomReserved
2013-11-28 00:15:50 +08:00
targetRow = None
2013-11-24 13:59:15 +08:00
if c [ 4 ] in ( 1 , 2 ) :
2013-12-02 22:38:44 +08:00
while row < rowmax and res < c [ 7 ] :
2013-11-28 00:15:50 +08:00
if targetRow != rows [ c [ 4 ] ] [ row ] :
targetRow = rows [ c [ 4 ] ] [ row ]
if targetRow and targetRow [ 0 ] + lifetime > c [ 0 ] :
break
row + = 1
res + = 1
2013-11-24 13:59:15 +08:00
else :
try :
thresholdTime = c [ 0 ] - lifetime * ( 1 - width / ( c [ 8 ] + width ) )
except ZeroDivisionError :
thresholdTime = c [ 0 ] - lifetime
2013-12-02 22:38:44 +08:00
while row < rowmax and res < c [ 7 ] :
2013-11-28 00:15:50 +08:00
if targetRow != rows [ c [ 4 ] ] [ row ] :
targetRow = rows [ c [ 4 ] ] [ row ]
try :
if targetRow and ( targetRow [ 0 ] > thresholdTime or targetRow [ 0 ] + targetRow [ 8 ] * lifetime / ( targetRow [ 8 ] + width ) > c [ 0 ] ) :
break
except ZeroDivisionError :
pass
row + = 1
res + = 1
2013-11-02 19:58:10 +08:00
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-11-17 14:59:25 +08:00
Script Updated By : Danmaku2ASS ( https : / / github . com / m13253 / danmaku2ass )
2013-09-15 13:11:58 +08:00
ScriptType : v4 .00 +
2013-11-17 14:59:25 +08:00
WrapStyle : 2
2013-09-15 13:11:58 +08: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-17 09:09:13 +08:00
Style : % ( styleid ) s , % ( fontface ) s , % ( fontsize ) s , & H % ( alpha ) 02 XFFFFFF , & H % ( alpha ) 02 XFFFFFF , & H % ( alpha ) 02 X000000 , & H % ( alpha ) 02 X000000 , 0 , 0 , 0 , 0 , 100 , 100 , 0.00 , 0.00 , 1 , % ( outline ) s , 0 , 7 , 0 , 0 , 0 , 0
2013-09-15 13:11:58 +08:00
[ Events ]
Format : Layer , Start , End , Style , Name , MarginL , MarginR , MarginV , Effect , Text
2013-11-17 09:09:13 +08:00
''' % { ' width ' : width, ' height ' : height, ' fontface ' : fontface, ' fontsize ' : round(fontsize), ' alpha ' : 255-round(alpha*255), ' outline ' : round(fontsize/25), ' 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-17 14:59:25 +08:00
text = ASSEscape ( c [ 3 ] )
2013-11-16 22:37:58 +08:00
styles = [ ]
2013-11-02 19:58:10 +08:00
if c [ 4 ] == 1 :
2013-11-16 22:37:58 +08:00
styles . append ( ' \\ an8 \\ pos( %(halfwidth)s , %(row)s ) ' % { ' halfwidth ' : round ( width / 2 ) , ' row ' : row } )
2013-11-02 19:58:10 +08:00
elif c [ 4 ] == 2 :
2013-11-16 22:37:58 +08:00
styles . append ( ' \\ an2 \\ pos( %(halfwidth)s , %(row)s ) ' % { ' halfwidth ' : round ( width / 2 ) , ' row ' : ConvertType2 ( row , height , bottomReserved ) } )
2013-11-17 13:52:46 +08:00
elif c [ 4 ] == 3 :
styles . append ( ' \\ move( %(neglen)s , %(row)s , %(width)s , %(row)s ) ' % { ' width ' : width , ' row ' : row , ' neglen ' : - math . ceil ( c [ 8 ] ) } )
2013-11-02 19:58:10 +08:00
else :
2013-11-16 22:37:58 +08:00
styles . append ( ' \\ move( %(width)s , %(row)s , %(neglen)s , %(row)s ) ' % { ' width ' : width , ' row ' : row , ' neglen ' : - math . ceil ( c [ 8 ] ) } )
2013-11-02 19:58:10 +08:00
if not ( - 1 < c [ 6 ] - fontsize < 1 ) :
2013-11-16 22:37:58 +08:00
styles . append ( ' \\ fs %s ' % round ( c [ 6 ] ) )
2013-11-02 19:58:10 +08:00
if c [ 5 ] != 0xffffff :
2013-11-16 22:37:58 +08:00
styles . append ( ' \\ c&H %02X %02X %02x & ' % ( c [ 5 ] & 0xff , ( c [ 5 ] >> 8 ) & 0xff , ( c [ 5 ] >> 16 ) & 0xff ) )
2013-11-02 19:58:10 +08:00
if c [ 5 ] == 0x000000 :
2013-11-16 22:37:58 +08:00
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 ] + lifetime ) , ' styles ' : ' ' . join ( styles ) , ' text ' : text , ' styleid ' : styleid } )
2013-09-15 13:11:58 +08:00
2013-11-17 14:59:25 +08:00
def ASSEscape ( s ) :
return ' \\ N ' . join ( ( i or ' ' for i in str ( s ) . replace ( ' \\ ' , ' \\ \\ ' ) . replace ( ' { ' , ' \\ { ' ) . replace ( ' } ' , ' \\ } ' ) . split ( ' \n ' ) ) )
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 ) :
2013-11-16 22:11:31 +08:00
timestamp = round ( timestamp * 100.0 )
hour , minute = divmod ( timestamp , 360000 )
minute , second = divmod ( minute , 6000 )
second , centsecond = divmod ( second , 100 )
2013-11-08 19:54:09 +08:00
return ' %d : %02d : %02d . %02d ' % ( int ( hour ) , int ( minute ) , int ( second ) , int ( centsecond ) )
2013-11-02 19:58:10 +08:00
2013-09-15 13:11:58 +08:00
def ConvertType2 ( row , height , bottomReserved ) :
return height - bottomReserved - row
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-16 13:51:32 +08:00
class safe_list ( list ) :
def get ( self , index , default = None ) :
try :
return self [ index ]
except IndexError :
return default
2013-11-08 19:47:11 +08:00
def export ( func ) :
global __all__
try :
__all__ . append ( func . __name__ )
except NameError :
__all__ = [ func . __name__ ]
return func
@export
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-12-01 00:28:30 +08:00
fo = None
2013-11-08 19:47:11 +08:00
comments = ReadComments ( input_files , font_size )
2013-11-02 20:26:01 +08:00
try :
if output_file :
fo = ConvertToFile ( output_file , ' w ' , encoding = ' utf-8 ' , newline = ' \r \n ' )
else :
fo = sys . stdout
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 :
2013-12-01 00:28:30 +08:00
if output_file and fo :
2013-11-02 20:26:01 +08:00
fo . close ( )
2013-11-08 19:47:11 +08:00
@export
def ReadComments ( input_files , font_size = 25.0 , progress_callback = None ) :
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 ' ) as f :
CommentProcessor = GetCommentProcessor ( f )
if not CommentProcessor :
raise ValueError ( _ ( ' Unknown comment file format: %s ' ) % i )
2013-11-16 23:15:44 +08:00
comments . extend ( CommentProcessor ( FilterBadChars ( f ) , font_size ) )
2013-11-08 19:47:11 +08:00
if progress_callback :
progress_callback ( len ( input_files ) , len ( input_files ) )
comments . sort ( )
return comments
@export
def GetCommentProcessor ( input_file ) :
return CommentFormatMap [ ProbeCommentFormat ( input_file ) ]
2013-11-02 19:58:10 +08:00
def main ( ) :
2013-11-24 17:07:14 +08:00
if len ( sys . argv ) == 1 :
sys . argv . append ( ' --help ' )
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 ' ) )
2013-11-24 15:37:59 +08:00
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 )
2013-09-30 23:27:33 +08:00
parser . add_argument ( ' -a ' , ' --alpha ' , metavar = _ ( ' ALPHA ' ) , help = _ ( ' Text opaque ' ) , type = float , default = 1.0 )
2013-11-24 15:37:59 +08:00
parser . add_argument ( ' -l ' , ' --lifetime ' , metavar = _ ( ' SECONDS ' ) , help = _ ( ' Duration of comment display [default: %s ] ' ) % 5 , type = float , default = 5.0 )
2013-09-30 23:27:33 +08:00
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 ( )