2013-09-15 09:39:05 +08:00
#!/usr/bin/env python3
2013-12-15 15:20:42 +08:00
# The original author of this program, Danmaku2ASS, is StarBrilliant.
# This file is released under General Public License version 3.
# You should have received a copy of General Public License text alongside with
# this program. If not, you can obtain it at http://gnu.org/copyleft/gpl.html .
# This program comes with no warranty, the author will not be resopnsible for
# any damage or problems caused by this program.
2014-11-08 00:51:55 +08:00
# You can obtain a latest copy of Danmaku2ASS at:
# https://github.com/m13253/danmaku2ass
# Please update to the latest version before complaining.
2013-09-15 09:39:05 +08:00
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-12-09 00:24:08 +08:00
# It is unwise to wrap a JSON object in an array!
# See this: http://haacked.com/archive/2008/11/20/anatomy-of-a-subtle-json-vulnerability.aspx/
# Do never follow what Acfun developers did!
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 '
2015-12-26 12:13:12 +08:00
elif tmp . strip ( ) . startswith ( ' " result ' ) :
return ' Tudou2 '
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 < ' :
2015-01-28 22:08:53 +08:00
tmp = f . read ( 20 )
if tmp == ' !-- BoonSutazioData= ' :
return ' Niconico ' # Niconico videos downloaded with NicoFox
else :
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 ) :
2016-05-18 05:16:04 +08:00
#comment_element = json.load(f)
2015-07-05 21:45:36 +08:00
# after load acfun comment json file as python list, flatten the list
2016-05-18 05:16:04 +08:00
#comment_element = [c for sublist in comment_element for c in sublist]
comment_elements = json . load ( f )
comment_element = comment_elements [ 2 ]
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
size = int ( p [ 3 ] ) * fontsize / 25.0
2013-12-07 23:38:24 +08:00
if p [ 2 ] != ' 7 ' :
2013-12-08 00:31:03 +08:00
c = str ( comment [ ' m ' ] ) . replace ( ' \\ r ' , ' \n ' ) . replace ( ' \r ' , ' \n ' )
2013-12-07 23:38:24 +08:00
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 :
2013-12-08 00:31:03 +08:00
c = dict ( json . loads ( comment [ ' m ' ] ) )
2013-12-07 23:38:24 +08:00
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
2015-02-16 14:24:05 +08:00
assert p [ 1 ] in ( ' 1 ' , ' 4 ' , ' 5 ' , ' 6 ' , ' 7 ' , ' 8 ' )
2016-08-12 12:30:07 +08:00
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 ' : # 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 )
elif p [ 1 ] == ' 8 ' :
pass # ignore scripted comment
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
2015-12-26 12:13:12 +08:00
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 ' {} ' )
2016-05-09 09:53:28 +08:00
size = int ( prop . get ( ' size ' , 1 ) )
2015-12-26 12:13:12 +08:00
assert size in ( 0 , 1 , 2 )
size = { 0 : 0.64 , 1 : 1 , 2 : 1.44 } [ size ] * fontsize
2016-05-09 09:53:28 +08:00
pos = int ( prop . get ( ' pos ' , 3 ) )
2015-12-26 12:13:12 +08:00
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
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
2016-08-13 16:28:28 +08:00
CommentFormatMap = { None : None , ' Niconico ' : ReadCommentsNiconico , ' Acfun ' : ReadCommentsAcfun , ' Bilibili ' : ReadCommentsBilibili , ' Tudou ' : ReadCommentsTudou , ' Tudou2 ' : ReadCommentsTudou2 , ' MioMio ' : ReadCommentsMioMio }
2013-11-02 20:11:38 +08:00
2013-11-16 13:51:32 +08:00
def WriteCommentBilibiliPositioned ( f , c , width , height , styleid ) :
2014-04-26 21:47:13 +08:00
#BiliPlayerSize = (512, 384) # Bilibili player version 2010
#BiliPlayerSize = (540, 384) # Bilibili player version 2012
2014-04-12 19:04:42 +08:00
BiliPlayerSize = ( 672 , 438 ) # Bilibili player version 2014
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 )
2014-06-10 20:09:24 +08:00
from_x = GetPosition ( from_x , False )
from_y = GetPosition ( from_y , True )
to_x = GetPosition ( to_x , False )
to_y = GetPosition ( to_y , True )
2013-11-16 22:06:10 +08:00
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 )
2014-04-12 15:04:41 +08:00
rotate_z = int ( comment_args . get ( 5 , 0 ) )
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 ' )
2014-06-08 19:13:25 +08:00
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 )
2014-06-10 20:09:24 +08:00
styles = [ ' \\ org( %d , %d ) ' % ( width / 2 , height / 2 ) ]
2014-06-08 19:13:25 +08:00
if from_rotarg [ 0 : 2 ] == to_rotarg [ 0 : 2 ] :
2014-06-10 20:09:24 +08:00
styles . append ( ' \\ pos( %.0f , %.0f ) ' % ( from_rotarg [ 0 : 2 ] ) )
2013-11-16 22:06:10 +08:00
else :
2014-06-10 20:09:24 +08:00
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 ] ) )
2014-04-12 15:04:41 +08:00
if ( from_x , from_y ) != ( to_x , to_y ) :
2014-06-10 20:09:24 +08:00
styles . append ( ' \\ t( %d , %d , ' % ( delay , delay + duration ) )
styles . append ( ' \\ frx %.0f \\ fry %.0f \\ frz %.0f \\ fscx %.0f \\ fscy %.0f ' % ( to_rotarg [ 2 : 7 ] ) )
2014-04-12 15:04:41 +08:00
styles . append ( ' ) ' )
2013-11-16 13:51:32 +08:00
if fontface :
2013-12-07 23:38:24 +08:00
styles . append ( ' \\ fn %s ' % ASSEscape ( fontface ) )
2014-06-10 20:09:24 +08:00
styles . append ( ' \\ fs %.0f ' % ( c [ 6 ] * ZoomFactor [ 0 ] ) )
2013-11-16 13:51:32 +08:00
if c [ 5 ] != 0xffffff :
2014-06-10 22:16:56 +08:00
styles . append ( ' \\ c&H %s & ' % ConvertColor ( c [ 5 ] ) )
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 ) :
2014-06-10 20:09:24 +08:00
styles . append ( ' \\ fad( %.0f ,0) ' % ( lifetime * 1000 ) )
2013-11-24 13:04:44 +08:00
elif ( from_alpha , to_alpha ) == ( 0 , 255 ) :
2014-06-10 20:09:24 +08:00
styles . append ( ' \\ fad(0, %.0f ) ' % ( lifetime * 1000 ) )
2013-11-16 22:06:10 +08:00
else :
2014-06-10 20:09:24 +08:00
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 } )
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 )
2014-02-03 16:54:30 +08:00
2013-12-07 23:38:24 +08:00
def WriteCommentAcfunPositioned ( f , c , width , height , styleid ) :
2013-12-09 00:24:08 +08:00
AcfunPlayerSize = ( 560 , 400 )
2013-12-07 23:38:24 +08:00
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 ]
2013-12-09 00:24:08 +08:00
def GetTransformStyles ( x = None , y = None , scale_x = None , scale_y = None , rotate_z = None , rotate_y = None , color = None , alpha = None ) :
2013-12-07 23:38:24 +08:00
styles = [ ]
2014-06-08 19:13:25 +08:00
out_x , out_y = x , y
2014-04-12 15:38:27 +08:00
if rotate_z is not None and rotate_y is not None :
assert x is not None
2014-04-21 23:48:53 +08:00
assert y is not None
2014-06-08 19:13:25 +08:00
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
2014-06-10 20:09:24 +08:00
styles . append ( ' \\ frx %.0f \\ fry %.0f \\ frz %.0f \\ fscx %.0f \\ fscy %.0f ' % ( rotarg [ 2 : 5 ] + ( rotarg [ 5 ] * scale_x , rotarg [ 6 ] * scale_y ) ) )
2014-06-08 19:13:25 +08:00
else :
if scale_x is not None :
2014-06-10 20:09:24 +08:00
styles . append ( ' \\ fscx %.0f ' % ( scale_x * 100 ) )
2014-06-08 19:13:25 +08:00
if s cale_y is not None :
2014-06-10 20:09:24 +08:00
styles . append ( ' \\ fscy %.0f ' % ( scale_y * 100 ) )
2013-12-09 00:24:08 +08:00
if color is not None :
2014-06-10 22:16:56 +08:00
styles . append ( ' \\ c&H %s & ' % ConvertColor ( color ) )
2013-12-09 00:24:08 +08:00
if color == 0x000000 :
styles . append ( ' \\ 3c&HFFFFFF& ' )
if alpha is not None :
alpha = 255 - round ( alpha * 255 )
styles . append ( ' \\ alpha&H %02X ' % alpha )
2014-06-08 19:13:25 +08:00
return out_x , out_y , styles
2013-12-09 00:24:08 +08:00
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 ]
2014-06-08 21:13:27 +08:00
text = ASSEscape ( str ( comment_args [ ' n ' ] ) . replace ( ' \r ' , ' \n ' ) )
2014-06-10 20:09:24 +08:00
common_styles = [ ' \ org( %d , %d ) ' % ( width / 2 , height / 2 ) ]
2013-12-09 00:24:08 +08:00
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 )
2013-12-07 23:38:24 +08:00
font = comment_args . get ( ' w ' )
if font :
font = dict ( font )
fontface = font . get ( ' f ' )
if fontface :
2013-12-09 00:24:08 +08:00
common_styles . append ( ' \\ fn %s ' % ASSEscape ( str ( fontface ) ) )
2013-12-07 23:38:24 +08:00
fontbold = bool ( font . get ( ' b ' ) )
if fontbold :
2013-12-09 00:24:08 +08:00
common_styles . append ( ' \\ b1 ' )
2014-06-10 20:09:24 +08:00
common_styles . append ( ' \\ fs %.0f ' % ( c [ 6 ] * ZoomFactor [ 0 ] ) )
2013-12-07 23:38:24 +08:00
isborder = bool ( comment_args . get ( ' b ' , True ) )
if not isborder :
2013-12-09 00:24:08 +08:00
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 ) )
2014-06-08 19:13:25 +08:00
to_scale_x = float ( comment_args . get ( ' e ' , 1.0 ) )
to_scale_y = float ( comment_args . get ( ' f ' , 1.0 ) )
2014-04-12 15:38:27 +08:00
to_rotate_z = float ( comment_args . get ( ' r ' , 0.0 ) )
to_rotate_y = float ( comment_args . get ( ' k ' , 0.0 ) )
2013-12-09 00:24:08 +08:00
to_color = c [ 5 ]
to_alpha = float ( comment_args . get ( ' a ' , 1.0 ) )
from_time = float ( comment_args . get ( ' t ' , 0.0 ) )
2013-12-07 23:38:24 +08:00
action_time = float ( comment_args . get ( ' l ' , 3.0 ) )
actions = list ( comment_args . get ( ' z ' , [ ] ) )
2014-06-08 19:13:25 +08:00
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 )
2014-06-10 20:09:24 +08:00
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 )
2014-06-08 19:13:25 +08:00
action_styles = transform_styles
2013-12-07 23:38:24 +08:00
for action in actions :
action = dict ( action )
2013-12-09 00:24:08 +08:00
from_x , from_y = to_x , to_y
2014-06-08 19:13:25 +08:00
from_out_x , from_out_y = to_out_x , to_out_y
2013-12-09 00:24:08 +08:00
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
2014-06-08 19:13:25 +08:00
transform_styles , action_styles = action_styles , [ ]
2013-12-09 00:24:08 +08:00
from_time + = action_time
action_time = float ( action . get ( ' l ' , 0.0 ) )
2013-12-07 23:38:24 +08:00
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 :
2014-06-08 19:13:25 +08:00
to_scale_x = float ( action [ ' f ' ] )
2013-12-07 23:38:24 +08:00
if ' g ' in action :
2014-06-08 19:13:25 +08:00
to_scale_y = float ( action [ ' g ' ] )
2013-12-08 00:31:03 +08:00
if ' c ' in action :
to_color = int ( action [ ' c ' ] )
if ' t ' in action :
2013-12-09 00:24:08 +08:00
to_alpha = float ( action [ ' t ' ] )
2013-12-08 00:31:03 +08:00
if ' d ' in action :
2014-04-12 15:38:27 +08:00
to_rotate_z = float ( action [ ' d ' ] )
2013-12-08 00:31:03 +08:00
if ' e ' in action :
2014-04-12 15:38:27 +08:00
to_rotate_y = float ( action [ ' e ' ] )
2014-06-08 19:13:25 +08:00
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 ) :
2014-06-10 20:09:24 +08:00
pos_style = ' \\ pos( %.0f , %.0f ) ' % ( to_out_x , to_out_y )
2013-12-09 00:24:08 +08:00
else :
2014-06-10 20:09:24 +08:00
pos_style = ' \\ move( %.0f , %.0f , %.0f , %.0f ) ' % ( from_out_x , from_out_y , to_out_x , to_out_y )
2014-06-08 19:13:25 +08:00
styles = common_styles + transform_styles
styles . append ( pos_style )
2013-12-07 23:38:24 +08:00
if action_styles :
2014-06-08 19:13:25 +08:00
styles . append ( ' \\ t( %s ) ' % ( ' ' . join ( action_styles ) ) )
FlushCommentLine ( f , text , styles , c [ 0 ] + from_time , c [ 0 ] + from_time + action_time , styleid )
2013-12-07 23:38:24 +08:00
except ( IndexError , ValueError ) as e :
logging . warning ( _ ( ' Invalid comment: %r ' ) % c [ 3 ] )
2014-02-03 19:23:09 +08:00
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
2014-02-03 16:54:30 +08:00
2014-04-12 14:33:31 +08:00
# Calculation is based on https://github.com/jabbany/CommentCoreLibrary/issues/5#issuecomment-40087282
2014-04-27 20:29:49 +08:00
# and https://github.com/m13253/danmaku2ass/issues/7#issuecomment-41489422
2014-06-10 22:21:15 +08:00
# ASS FOV = width*4/3.0
2014-06-10 22:25:02 +08:00
# But Flash FOV = width/math.tan(100*math.pi/360.0)/2 will be used instead
2014-06-05 21:50:00 +08:00
# Result: (transX, transY, rotX, rotY, rotZ, scaleX, scaleY)
def ConvertFlashRotation ( rotY , rotZ , X , Y , width , height ) :
2014-04-27 20:29:49 +08:00
def WrapAngle ( deg ) :
2014-06-15 15:35:27 +08:00
return 180 - ( ( 180 - deg ) % 360 )
2014-04-29 21:56:48 +08:00
rotY = WrapAngle ( rotY )
rotZ = WrapAngle ( rotZ )
2014-06-05 21:50:00 +08:00
if rotY in ( 90 , - 90 ) :
rotY - = 1
2014-04-30 20:36:09 +08:00
if rotY == 0 or rotZ == 0 :
2014-04-28 13:08:36 +08:00
outX = 0
outY = - rotY # Positive value means clockwise in Flash
outZ = - rotZ
2014-06-08 20:53:53 +08:00
rotY * = math . pi / 180.0
rotZ * = math . pi / 180.0
2014-04-28 13:08:36 +08:00
else :
2014-06-08 20:53:53 +08:00
rotY * = math . pi / 180.0
rotZ * = math . pi / 180.0
2014-04-28 13:08:36 +08:00
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
2014-04-30 20:36:09 +08:00
outX = math . asin ( math . sin ( rotY ) * math . sin ( rotZ ) ) * 180 / math . pi
2014-06-08 20:32:35 +08:00
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
2014-06-08 19:13:25 +08:00
trY = Y * math . cos ( rotZ ) - X * math . sin ( rotZ ) + math . sin ( rotZ ) * width / 2 + ( 1 - math . cos ( rotZ ) ) * height / 2
2014-06-08 20:32:35 +08:00
trZ = ( trX - width / 2 ) * math . sin ( rotY )
2014-06-10 22:21:15 +08:00
FOV = width * math . tan ( 2 * math . pi / 9.0 ) / 2
2014-06-10 20:09:49 +08:00
try :
2014-06-10 19:46:48 +08:00
scaleXY = FOV / ( FOV + trZ )
except ZeroDivisionError :
2014-06-15 15:35:27 +08:00
logging . error ( ' Rotation makes object behind the camera: trZ == %.0f ' % trZ )
2014-06-08 20:59:22 +08:00
scaleXY = 1
2014-06-11 00:01:45 +08:00
trX = ( trX - width / 2 ) * scaleXY + width / 2
trY = ( trY - height / 2 ) * scaleXY + height / 2
2014-06-10 19:46:48 +08:00
if scaleXY < 0 :
scaleXY = - scaleXY
2014-06-11 00:01:45 +08:00
outX + = 180
outY + = 180
2014-06-15 15:35:27 +08:00
logging . error ( ' Rotation makes object behind the camera: trZ == %.0f < %.0f ' % ( trZ , FOV ) )
2014-06-10 20:09:24 +08:00
return ( trX , trY , WrapAngle ( outX ) , WrapAngle ( outY ) , WrapAngle ( outZ ) , scaleXY * 100 , scaleXY * 100 )
2014-04-12 14:33:31 +08:00
2014-12-21 01:23:52 +08:00
def ProcessComments ( comments , f , width , height , bottomReserved , fontface , fontsize , alpha , duration_marquee , duration_still , 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 :
2014-12-21 01:23:52 +08:00
freerows = TestFreeRows ( rows , i , row , width , height , bottomReserved , duration_marquee , duration_still )
2013-11-16 13:51:32 +08:00
if freerows > = i [ 7 ] :
MarkCommentRow ( rows , i , row )
2014-12-21 01:23:52 +08:00
WriteComment ( f , i , row , width , height , bottomReserved , fontsize , duration_marquee , duration_still , styleid )
2013-11-16 13:51:32 +08:00
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 )
2014-12-21 01:23:52 +08:00
WriteComment ( f , i , row , width , height , bottomReserved , fontsize , duration_marquee , duration_still , styleid )
2013-11-16 13:51:32 +08:00
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
2014-12-21 01:23:52 +08:00
def TestFreeRows ( rows , c , row , width , height , bottomReserved , duration_marquee , duration_still ) :
2013-11-02 19:58:10 +08:00
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 ]
2014-12-21 01:23:52 +08:00
if targetRow and targetRow [ 0 ] + duration_still > c [ 0 ] :
2013-11-28 00:15:50 +08:00
break
row + = 1
res + = 1
2013-11-24 13:59:15 +08:00
else :
try :
2014-12-21 01:23:52 +08:00
thresholdTime = c [ 0 ] - duration_marquee * ( 1 - width / ( c [ 8 ] + width ) )
2013-11-24 13:59:15 +08:00
except ZeroDivisionError :
2014-12-21 01:23:52 +08:00
thresholdTime = c [ 0 ] - duration_marquee
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 :
2014-12-21 01:23:52 +08:00
if targetRow and ( targetRow [ 0 ] > thresholdTime or targetRow [ 0 ] + targetRow [ 8 ] * duration_marquee / ( targetRow [ 8 ] + width ) > c [ 0 ] ) :
2013-11-28 00:15:50 +08:00
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 (
2015-11-19 10:00:11 +08:00
''' [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 +
2014-06-14 22:55:57 +08:00
PlayResX : % ( width ) d
PlayResY : % ( height ) d
2014-06-14 23:50:07 +08:00
Aspect Ratio : % ( width ) d : % ( height ) d
2014-06-10 22:16:56 +08:00
Collisions : Normal
WrapStyle : 2
2013-12-20 20:52:08 +08:00
ScaledBorderAndShadow : yes
2014-06-15 15:16:56 +08:00
YCbCr Matrix : TV .601
2013-09-15 13:11:58 +08:00
[ 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
2014-06-14 22:55:57 +08:00
Style : % ( styleid ) s , % ( fontface ) s , % ( fontsize ) .0 f , & 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 ) .0 f , 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
2014-06-15 00:35:55 +08:00
''' % { ' width ' : width, ' height ' : height, ' fontface ' : fontface, ' fontsize ' : fontsize, ' alpha ' : 255-round(alpha*255), ' outline ' : max(fontsize/25.0, 1), ' styleid ' : styleid}
2013-09-15 13:11:58 +08:00
)
2014-12-21 01:23:52 +08:00
def WriteComment ( f , c , row , width , height , bottomReserved , fontsize , duration_marquee , duration_still , 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 :
2014-06-10 20:09:24 +08:00
styles . append ( ' \\ an8 \\ pos( %(halfwidth)d , %(row)d ) ' % { ' halfwidth ' : width / 2 , ' row ' : row } )
2014-12-21 01:23:52 +08:00
duration = duration_still
2013-11-02 19:58:10 +08:00
elif c [ 4 ] == 2 :
2014-06-10 20:09:24 +08:00
styles . append ( ' \\ an2 \\ pos( %(halfwidth)d , %(row)d ) ' % { ' halfwidth ' : width / 2 , ' row ' : ConvertType2 ( row , height , bottomReserved ) } )
2014-12-21 01:23:52 +08:00
duration = duration_still
2013-11-17 13:52:46 +08:00
elif c [ 4 ] == 3 :
2014-06-10 20:09:24 +08:00
styles . append ( ' \\ move( %(neglen)d , %(row)d , %(width)d , %(row)d ) ' % { ' width ' : width , ' row ' : row , ' neglen ' : - math . ceil ( c [ 8 ] ) } )
2014-12-21 01:23:52 +08:00
duration = duration_marquee
2013-11-02 19:58:10 +08:00
else :
2014-06-10 20:09:24 +08:00
styles . append ( ' \\ move( %(width)d , %(row)d , %(neglen)d , %(row)d ) ' % { ' width ' : width , ' row ' : row , ' neglen ' : - math . ceil ( c [ 8 ] ) } )
2014-12-21 01:23:52 +08:00
duration = duration_marquee
2013-11-02 19:58:10 +08:00
if not ( - 1 < c [ 6 ] - fontsize < 1 ) :
2014-06-10 20:09:24 +08:00
styles . append ( ' \\ fs %.0f ' % c [ 6 ] )
2013-11-02 19:58:10 +08:00
if c [ 5 ] != 0xffffff :
2014-06-10 22:16:56 +08:00
styles . append ( ' \\ c&H %s & ' % ConvertColor ( c [ 5 ] ) )
2013-11-02 19:58:10 +08:00
if c [ 5 ] == 0x000000 :
2013-11-16 22:37:58 +08:00
styles . append ( ' \\ 3c&HFFFFFF& ' )
2014-12-21 01:23:52 +08:00
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 } )
2013-09-15 13:11:58 +08:00
2013-11-17 14:59:25 +08:00
def ASSEscape ( s ) :
2014-06-08 21:13:27 +08:00
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 ' ) ) )
2013-11-17 14:59:25 +08:00
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
2014-06-10 22:16:56 +08:00
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 : # VobSub always uses BT.601 colorspace, convert to BT.709
2014-06-15 15:35:27 +08:00
ClipByte = lambda x : 255 if x > 255 else 0 if x < 0 else round ( x )
2014-06-10 22:16:56 +08:00
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 )
)
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 ) :
2013-12-21 23:49:17 +08:00
if isinstance ( filename_or_file , bytes ) :
filename_or_file = str ( bytes ( filename_or_file ) . decode ( ' utf-8 ' , ' replace ' ) )
2013-11-02 20:26:01 +08:00
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
2016-08-13 17:02:37 +08:00
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 , is_reduce_comments = False , progress_callback = None ) :
2013-12-01 00:28:30 +08:00
fo = None
2016-08-13 17:02:37 +08:00
comments = ReadComments ( input_files , input_format , font_size )
2013-11-02 20:26:01 +08:00
try :
if output_file :
2013-12-16 00:08:04 +08:00
fo = ConvertToFile ( output_file , ' w ' , encoding = ' utf-8-sig ' , errors = ' replace ' , newline = ' \r \n ' )
2013-11-02 20:26:01 +08:00
else :
fo = sys . stdout
2014-12-21 01:23:52 +08:00
ProcessComments ( comments , fo , stage_width , stage_height , reserve_blank , font_face , font_size , text_opacity , duration_marquee , duration_still , is_reduce_comments , progress_callback )
2013-11-02 20:26:01 +08:00
finally :
2014-04-06 23:01:02 +08:00
if output_file and fo != output_file :
2013-11-02 20:26:01 +08:00
fo . close ( )
2013-11-08 19:47:11 +08:00
@export
2016-08-13 17:02:37 +08:00
def ReadComments ( input_files , input_format , font_size = 25.0 , progress_callback = None ) :
2013-12-21 23:49:17 +08:00
if isinstance ( input_files , bytes ) :
input_files = str ( bytes ( input_files ) . decode ( ' utf-8 ' , ' replace ' ) )
2013-11-08 19:47:11 +08:00
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 ) )
2013-12-16 00:08:04 +08:00
with ConvertToFile ( i , ' r ' , encoding = ' utf-8 ' , errors = ' replace ' ) as f :
2016-07-26 12:08:22 +08:00
s = f . read ( )
str_io = io . StringIO ( s )
2016-08-13 17:02:37 +08:00
if input_format == ' autodetect ' :
CommentProcessor = GetCommentProcessor ( str_io )
2016-08-13 17:20:02 +08:00
if not CommentProcessor :
raise ValueError (
_ ( ' Failed to detect comment file format: %s ' ) % i
)
2016-08-13 17:02:37 +08:00
else :
CommentProcessor = CommentFormatMap . get ( input_format )
2016-08-13 17:20:02 +08:00
if not CommentProcessor :
raise ValueError (
_ ( ' Unknown comment file format: %s ' ) % input_format
)
2016-07-26 12:08:22 +08:00
comments . extend ( CommentProcessor ( FilterBadChars ( str_io ) , 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 ( ) :
2014-06-14 20:30:11 +08:00
logging . basicConfig ( format = ' %(levelname)s : %(message)s ' )
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 ( )
2016-08-13 17:02:37 +08:00
parser . add_argument ( ' -f ' , ' --format ' , metavar = _ ( ' FORMAT ' ) , help = _ ( ' Format of input [default: autodetect] ' ) , default = ' autodetect ' )
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 )
2014-02-03 18:54:40 +08:00
parser . add_argument ( ' -a ' , ' --alpha ' , metavar = _ ( ' ALPHA ' ) , help = _ ( ' Text opacity ' ) , type = float , default = 1.0 )
2014-12-21 01:23:52 +08:00
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 )
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 )
2016-08-13 17:02:37 +08:00
Danmaku2ASS ( args . file , args . format , args . output , width , height , args . protect , args . font , args . fontsize , args . alpha , args . duration_marquee , args . duration_still , args . reduce )
2013-11-02 19:58:10 +08:00
if __name__ == ' __main__ ' :
main ( )