-
Notifications
You must be signed in to change notification settings - Fork 1
/
ASFMKV_py1.01.py
1505 lines (1444 loc) · 69.7 KB
/
ASFMKV_py1.01.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# -*- coding: UTF-8 -*-
# *************************************************************************
#
# 请使用支持 UTF-8 NoBOM 并最好带有 Python 语法高亮的文本编辑器
# Windows 7 的用户请最好不要使用 写字板/记事本 打开本脚本
#
# *************************************************************************
# 调用库,请不要修改
import shutil
from fontTools import ttLib
from fontTools import subset
from chardet.universaldetector import UniversalDetector
import os
from os import path
import sys
import re
import winreg
import zlib
import json
from colorama import init
from datetime import datetime
# 初始化环境变量
# *************************************************************************
# 自定义变量
# 修改注意: Python的布尔类型首字母要大写 True 或 False,在名称中有单引号的,需要输入反斜杠转义 \'
# *************************************************************************
# extlist 可输入的视频媒体文件扩展名
extlist = 'mkv;mp4;mts;mpg;flv;mpeg;m2ts;avi;webm;rm;rmvb;mov;mk3d;vob'
# *************************************************************************
# mkvout 媒体文件输出目录(封装)
# 在最前方用"?"标记来表示这是一个子目录
# 注意: 在Python中需要在左侧引号前加 r 来保留 Windows 路径中的反斜杠,路径末尾不需要反斜杠
mkvout = ''
# *************************************************************************
# assout 字幕文件输出目录
# 在最前方用"?"标记来表示这是一个子目录
# 注意: 在Python中需要在左侧引号前加 r 来保留 Windows 路径中的反斜杠,路径末尾不需要反斜杠
assout = '?subs'
# *************************************************************************
# fontout 字体文件输出目录
# 在最前方用"?"标记来表示这是一个子目录
# 注意: 在Python中需要在左侧引号前加 r 来保留 Windows 路径中的反斜杠,路径末尾不需要反斜杠
fontout = '?Fonts'
# *************************************************************************
# fontin 自定义字体文件夹,可做额外字体源,必须是绝对路径
# 可以有多个路径,路径之间用"?"来分隔
# 注意: 在Python中需要在左侧引号前加 r 来保留 Windows 路径中的反斜杠,路径末尾不需要反斜杠
fontin = r''
# *************************************************************************
# notfont (封装)字体嵌入
# True 始终嵌入字体
# False 不嵌入字体,不子集化字体,不替换字幕中的字体信息
notfont = False
# *************************************************************************
# sublang (封装)字幕语言
# 会按照您所输入的顺序给字幕赋予语言编码,如果字幕数多于语言数,多出部分将赋予最后一种语言编码
# IDX+SUB 的 DVDSUB 由于 IDX 文件一般有语言信息,对于DVDSUB不再添加语言编码
# 各语言之间应使用半角分号 ; 隔开,如 'chi;chi;chi;und'
# 可以在 mkvmerge -l 了解语言编码
# 可以只有一个 # 号让程序在运行时再询问您,如 '#'
sublang = ''
# *************************************************************************
# matchStrict 严格匹配
# True 媒体文件名必须在字幕文件名的最前方,如'test.mkv'的字幕可以是'test.ass'或是'test.sub.ass',但不能是'sub.test.ass'
# False 只要字幕文件名中有媒体文件名就行了,不管它在哪
matchStrict = True
# *************************************************************************
# rmAssIn (封装)如果输入文件是mkv,删除mkv文件中原有的字幕
rmAssIn = True
# *************************************************************************
# rmAttach (封装)如果输入文件是mkv,删除mkv文件中原有的附件
rmAttach = True
# *************************************************************************
# v_subdir 视频的子目录搜索
v_subdir = False
# *************************************************************************
# s_subdir 字幕的子目录搜索
s_subdir = False
# *************************************************************************
# copyfont (ListAssFont)拷贝字体到源文件夹
copyfont = False
# *************************************************************************
# resultw (ListAssFont)打印结果到源文件夹
resultw = False
# *************************************************************************
# 以下变量谨慎更改
# subext 可输入的字幕扩展名,按照python列表语法
subext = ['ass', 'ssa', 'srt', 'sup', 'idx']
# 以下环境变量不应更改
# 编译 style行 搜索用正则表达式
style_read = re.compile('.*\nStyle:.*')
cillegal = re.compile(r'[\\/:\*"><\|]')
# 切分extlist列表
extlist = [s.strip(' ').lstrip('.').lower() for s in extlist.split(';') if len(s) > 0]
# 切分sublang列表
sublang = [s.strip(' ').lower() for s in sublang.split(';') if len(s) > 0]
# 切分fontin列表
fontin = [s.strip(' ') for s in fontin.split('?') if len(s) > 0]
fontin = [s for s in fontin if path.isdir(s)]
langlist = []
extsupp = []
dupfont = {}
init()
def fontlistAdd(s: str, fn: str, fontlist: dict) -> dict:
if len(s) > 0:
si = 0
fn = fn.lstrip('@')
if fontlist.get(fn) is None:
fontlist[fn] = s[0]
si += 1
for i in range(si, len(s)):
if not s[i] in fontlist[fn]:
fontlist[fn] = fontlist[fn] + s[i]
return fontlist
# ASS分析部分
# 需要输入
# asspath: ASS文件的绝对路径
# 可选输入
# fontlist: 可以更新fontlist,用于多ASS同时输入的情况,结构见下
# onlycheck: 只确认字幕中的字体,仅仅返回fontlist
# 将会返回
# fullass: 完整的ASS文件内容,以行分割
# fontlist: 字体与其所需字符 { 字体 : 字符串 }
# styleline: 样式内容的起始行
# font_pos: 字体在样式中的位置
# fn_lines: 带有fn标签的行数与该行的完整特效标签,一项一个 [ [行数, 标签1, 标签2], ... ]
def assAnalyze(asspath: str, fontlist: dict = {}, onlycheck: bool = False):
global style_read
# 初始化变量
eventline = 0
style_pos = 0
style_pos2 = 0
text_pos = 0
font_pos = 0
styleline = 0
fn_lines = []
# 编译分析用正则表达式
event_read = re.compile('.*\nDialogue:.*')
style = re.compile(r'^\[V4.*Styles\]$')
event = re.compile(r'^\[Events\]$')
# 识别文本编码并读取整个SubtitleStationAlpha文件到内存
print('\033[1;33m正在分析字幕: \033[1;37m\"{0}\"\033[0m'.format(path.basename(asspath)))
ass = open(asspath, mode='rb')
# if path.getsize(asspath) <= 100 * 1024:
# ass_b = ass.read()
# ass_code = chardet.detect(ass_b)['encoding'].lower()
# else:
detector = UniversalDetector()
for dt in ass:
detector.feed(dt)
if detector.done:
ass_code = detector.result['encoding']
break
detector.reset()
ass.close()
ass = open(asspath, encoding=ass_code, mode='r')
fullass = ass.readlines()
ass.close()
asslen = len(fullass)
# 在文件中搜索Styles标签和Events标签来确认起始行
for s in range(0, asslen):
if re.match(style, fullass[s]) is not None:
styleline = s
elif re.match(event, fullass[s]) is not None:
eventline = s
if styleline != 0 and eventline != 0:
break
# 获取Style的 Format 行,并用半角逗号分割
style_format = ''.join(fullass[styleline + 1].split(':')[1:]).strip(' ').split(',')
# 确定Style中 Name 和 Fontname 的位置
for i in range(0, len(style_format)):
if style_format[i].lower().strip(' ').replace('\n', '') == 'name':
style_pos = i
elif style_format[i].lower().strip(' ').replace('\n', '') == 'fontname':
font_pos = i
if style_pos != 0 and font_pos != 0:
break
# 获取 字体表 与 样式字体对应表
style_font = {}
# style_font 词典内容:
# { 样式 : 字体名 }
# fontlist 词典内容:
# { 字体名 : 使用该字体的文本 }
for i in range(styleline + 2, asslen):
if len(fullass[i].split(':')) < 2:
if i + 1 > asslen:
break
else:
if re.search(style_read, '\n'.join(fullass[i + 1:])) is None:
break
else:
continue
styleStr = ''.join(fullass[i].split(':')[1:]).strip(' ').split(',')
font_key = styleStr[font_pos].lstrip('@')
fontlist.setdefault(font_key, '')
style_font[styleStr[style_pos]] = styleStr[font_pos]
#print(fontlist)
# 提取Event的 Format 行,并用半角逗号分割
event_format = ''.join(fullass[eventline + 1].split(':')[1:]).strip(' ').split(',')
# 确定Event中 Style 和 Text 的位置
for i in range(0, len(event_format)):
if event_format[i].lower().replace('\n', '').strip(' ') == 'style':
style_pos2 = i
elif event_format[i].lower().replace('\n', '').strip(' ') == 'text':
text_pos = i
if style_pos2 != 0 and text_pos != 0:
break
# 获取 字体的字符集
# 先获取 Style,用style_font词典查找对应的 Font
# 再将字符串追加到 fontlist 中对应 Font 的值中
for i in range(eventline + 2, asslen):
eventline_sp = fullass[i].split(':')
if len(eventline_sp) < 2:
if i + 1 > asslen:
break
else:
if re.search(event_read, '\n'.join(fullass[i + 1:])) is None:
break
else:
continue
#print(fullass[i])
if eventline_sp[0].strip(' ').lower() == 'comment':
continue
eventline_sp = ''.join(eventline_sp[1:]).split(',')
eventftext = ','.join(eventline_sp[text_pos:])
effectDel = r'(\{.*?\})|(\\[hnN])|(\s)|(.*m .*\w.*)'
vecpos = []
effpos = []
for s in re.findall(r'{\\.*?}', eventftext):
if re.search(r'\\p[1-9][0-9]*', s) is not None:
vecpos.append(s)
else: effpos.append(s)
textremain = ''
lastend = 0
if len(vecpos) > 0:
for v in vecpos:
etext = re.sub(effectDel, '', eventftext[lastend:lastend + eventftext.find(v)].strip(' '))
# print(v)
lastend = eventftext.find(v) + len(v)
endvec = re.search(r'\\p0', eventftext[lastend:])
rmvecr = r'\\p[0-9]+'
rmvec = re.sub(rmvecr, '', eventftext[eventftext.find(v):lastend])
textremain = textremain + etext + rmvec
# print(1, textremain)
if endvec is not None:
endvec = endvec.span()
for s in effpos:
endpos = eventftext.find(s)
nextpos = endpos + len(effpos)
if endpos >= endvec[0] and nextpos <= endvec[1]:
endvecp = eventftext[nextpos:]
if len(endvecp) > 0:
textremain = textremain + re.sub(rmvecr, '', endvecp)
lastend += endvecp
else:
break
# print(2, textremain)
eventftext = eventftext[lastend:]
if len(textremain) > 0:
eventftext = textremain
eventfont = style_font.get(eventline_sp[style_pos2].lstrip('*'))
# print(eventftext)
#fneffect = re.findall(r'{\\fn.*?}', eventftext)
it = re.search(r'{\\fn.*?}', eventftext)
if it is not None:
fn = ''
fn_line = [i]
while it is not None:
it = it.span()
if it[0] > 0:
s = re.sub(effectDel, '', eventftext[:it[0]])
# print('add', s)
# print('fn', fn)
if len(fn) > 0:
fontlist = fontlistAdd(s, fn, fontlist)
else: fontlist = fontlistAdd(s, eventfont, fontlist)
s = eventftext[(it[0] + 1):(it[1] - 1)]
l = [sf.strip(' ') for sf in s.split('\\') if len(s.strip(' ')) > 0]
l.reverse()
for sf in l:
if 'fn' in sf.lower():
fn = sf[2:].strip(' ').lstrip('@')
fn_line.append(s)
break
eventftext = eventftext[it[1]:]
# print('ef', eventftext)
it = re.search(r'{\\fn.*?}', eventftext)
# os.system('pause')
else:
s = re.sub(effectDel, '', eventftext)
# print('add', s)
# print('fn', fn)
if len(fn) > 0:
fontlist = fontlistAdd(s, fn, fontlist)
else: fontlist = fontlistAdd(s, eventfont, fontlist)
fn_lines.append(fn_line)
else:
if not eventfont is None:
# 去除行中非文本部分,包括特效标签{},硬软换行符
eventtext = re.sub(r'(\{.*?\})|(\\[hnN])|(\s)', '', eventftext)
#print(eventfont, eventtext)
#print(eventtext, ','.join(eventline_sp[text_pos:]))
fontlist = fontlistAdd(eventtext, eventfont, fontlist)
if not onlycheck: print('\033[1m字幕所需字体\033[0m')
fl_popkey = []
# 在字体列表中检查是否有没有在文本中使用的字体,如果有,添加到删去列表
for s in fontlist.keys():
if len(fontlist[s]) == 0:
fl_popkey.append(s)
#print('跳过没有字符的字体\"{0}\"'.format(s))
else:
if not onlycheck: print('\033[1m\"{0}\"\033[0m: 字符数[\033[1;33m{1}\033[0m]'.format(s, len(fontlist[s])))
# 删去 删去列表 中的字体
if len(fl_popkey) > 0:
for s in fl_popkey:
fontlist.pop(s)
# 如果 onlycheck 为 True,只返回字体列表
if onlycheck:
del fullass
style_font.clear()
return None, fontlist, None, None, None
#os.system('pause')
return fullass, fontlist, styleline, font_pos, fn_lines
# 获取字体文件列表
# 接受输入
# customPath: 用户指定的字体文件夹
# font_name: 用于更新font_name(启用从注册表读取名称的功能时有效)
# noreg: 只从用户提供的customPath获取输入
# 将会返回
# filelist: 字体文件清单 [[ 字体绝对路径, 读取位置('': 注册表, '0': 自定义目录) ], ...]
# font_name: 用于更新font_name(启用从注册表读取名称的功能时有效)
def getFileList(customPath: list = [], font_name: dict = {}, noreg: bool = False):
filelist = []
if not noreg:
# 从注册表读取
fontkey = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, r'SOFTWARE\Microsoft\Windows NT\CurrentVersion\Fonts')
fontkey_num = winreg.QueryInfoKey(fontkey)[1]
#fkey = ''
try:
# 从用户字体注册表读取
fontkey10 = winreg.OpenKey(winreg.HKEY_CURRENT_USER, r'SOFTWARE\Microsoft\Windows NT\CurrentVersion\Fonts')
fontkey10_num = winreg.QueryInfoKey(fontkey10)[1]
if fontkey10_num > 0:
for i in range(fontkey10_num):
p = winreg.EnumValue(fontkey10, i)[1]
#n = winreg.EnumValue(fontkey10, i)[0]
if path.exists(p):
# test = n.split('&')
# if len(test) > 1:
# for i in range(0, len(test)):
# font_name[re.sub(r'\(.*?\)', '', test[i].strip(' '))] = [p, i]
# else: font_name[re.sub(r'\(.*?\)', '', n.strip(' '))] = [p, 0]
filelist.append([p, ''])
# test = path.splitext(path.basename(p))[0].split('&')
except:
pass
for i in range(fontkey_num):
# 从 系统字体注册表 读取
k = winreg.EnumValue(fontkey, i)[1]
#n = winreg.EnumValue(fontkey, i)[0]
pk = path.join(r'C:\Windows\Fonts', k)
if path.exists(pk):
# test = n.split('&')
# if len(test) > 1:
# for i in range(0, len(test)):
# font_name[re.sub(r'\(.*?\)', '', test[i].strip(' '))] = [pk, i]
# else: font_name[re.sub(r'\(.*?\)', '', n.strip(' '))] = [pk, 0]
filelist.append([pk, ''])
# 从定义的文件夹读取
# fontspath = [r'C:\Windows\Fonts', path.join(os.getenv('USERPROFILE'),r'AppData\Local\Microsoft\Windows\Fonts')]
if customPath is None: customPath == []
if len(customPath) > 0:
print('\033[1;33m请稍等,正在获取自定义文件夹中的字体\033[0m')
for s in customPath:
if not path.isdir(s): continue
for r, d, f in os.walk(s):
for p in f:
p = path.join(r, p)
if path.splitext(p)[1][1:].lower() not in ['ttf', 'ttc', 'otc', 'otf']: continue
filelist.append([path.join(s, p), 'xxx'])
#print(font_name)
#os.system('pause')
return filelist, font_name
#字体处理部分
# 需要输入
# fl: 字体文件列表
# 可选输入
# f_n: 默认新建一个,可用于更新font_name
# 将会返回
# font_name: 字体内部名称与绝对路径的索引词典
# 会对以下全局变量进行变更
# dupfont: 重复字体的名称与其路径词典
# font_name 词典结构
# { 字体名称 : [ 字体绝对路径 , 字体索引 (仅用于TTC/OTC; 如果是TTF/OTF,默认为0) ] }
# dupfont 词典结构
# { 重复字体名称 : [ 字体1绝对路径, 字体2绝对路径, ... ] }
def fontProgress(fl: list, f_n: dict = {}) -> dict:
global dupfont
#print(fl)
flL = len(fl)
print('\033[1;32m正在读取字体信息...\033[0m')
for si in range(0, flL):
s = fl[si][0]
fromCustom = False
if len(fl[si][1]) > 0: fromCustom = True
# 如果有来自自定义文件夹标记,则将 fromCustom 设为 True
ext = path.splitext(s)[1][1:]
# 检查字体扩展名
if ext.lower() not in ['ttf','ttc','otf','otc']: continue
# 输出进度
print('\r' + '\033[1;32m{0}/{1} {2:.2f}% \033[0m'.format(si + 1, flL, ((si + 1)/flL)*100, ), end='', flush=True)
if ext.lower() in ['ttf', 'otf']:
# 如果是 TTF/OTF 单字体文件,则使用 TTFont 读取
try:
tc = [ttLib.TTFont(s, lazy=True)]
except:
print('\033[1;31m\n[ERROR] \"{0}\": {1}\n\033[1;34m[TRY] 正在尝试使用TTC/OTC模式读取\033[0m'.format(s, sys.exc_info()))
# 如果 TTFont 读取失败,可能是使用了错误扩展名的 TTC/OTC 文件,换成 TTCollection 尝试读取
try:
tc = ttLib.TTCollection(s, lazy=True)
print('\033[1;34m[WARNING] 错误的字体扩展名\"{0}\" \033[0m'.format(s))
except:
print('\033[1;31m\n[ERROR] \"{0}\": {1}\033[0m'.format(s, sys.exc_info()))
continue
else:
try:
# 如果是 TTC/OTC 字体集合文件,则使用 TTCollection 读取
tc = ttLib.TTCollection(s, lazy=True)
except:
print('\033[1;31m\n[ERROR] \"{0}\": {1}\n\033[1;34m[TRY] 正在尝试使用TTF/OTF模式读取\033[0m'.format(s, sys.exc_info()))
try:
# 如果读取失败,可能是使用了错误扩展名的 TTF/OTF 文件,用 TTFont 尝试读取
tc = [ttLib.TTFont(s, lazy=True)]
print('\033[1;34m[WARNING] 错误的字体扩展名\"{0}\" \033[0m'.format(s))
except:
print('\033[1;31m\n[ERROR] \"{0}\": {1}\033[0m'.format(s, sys.exc_info()))
continue
#f_n[path.splitext(path.basename(s))[0]] = [s, 0]
for ti in range(0, len(tc)):
t = tc[ti]
# 读取字体的 'name' 表
for ii in range(0, len(t['name'].names)):
name = t['name'].names[ii]
# 若 nameID 为 4,读取 NameRecord 的文本信息
if name.nameID == 4:
namestr = ''
try:
namestr = name.toStr()
except:
# 如果 fontTools 解码失败,则尝试使用 utf-16-be 直接解码
namestr = name.toBytes().decode('utf-16-be', errors='ignore')
try:
# 尝试使用 去除 \x00 字符 解码
if len([i for i in name.toBytes() if i == 0]) > 0:
nnames = t['name']
namebyte = b''.join([bytes.fromhex('{:0>2}'.format(hex(i)[2:])) for i in name.toBytes() if i > 0])
nnames.setName(namebyte,
name.nameID, name.platformID, name.platEncID, name.langID)
namestr = nnames.names[ii].toStr()
print('\n\033[1;33m已修正字体\"{0}\"名称读取 >> \"{1}\"\033[0m'.format(path.basename(s), namestr))
#os.system('pause')
else: namestr = name.toBytes().decode('utf-16-be', errors='ignore')
# 如果没有 \x00 字符,使用 utf-16-be 强行解码;如果有,尝试解码;如果解码失败,使用 utf-16-be 强行解码
except:
print('\n\033[1;33m尝试修正字体\"{0}\"名称读取 >> \"{1}\"\033[0m'.format(path.basename(s), namestr))
if namestr is None: continue
namestr = namestr.strip(' ')
#print(namestr, path.basename(s))
if f_n.get(namestr) is not None:
# 如果发现列表中已有相同名称的字体,检测它的文件名、扩展名、父目录是否相同
# 如果有一者不同且不来自自定义文件夹,添加到重复字体列表
dupp = f_n[namestr][0]
if dupp != s and path.splitext(path.basename(dupp))[0] != path.splitext(path.basename(s))[0] and not fromCustom:
print('\033[1;35m[WARNING] 字体\"{0}\"与字体\"{1}\"的名称\"{2}\"重复!\033[0m'.format(path.basename(f_n[namestr][0]), path.basename(s), namestr))
if dupfont.get(namestr) is not None:
if s not in dupfont[namestr]:
dupfont[namestr].append(s)
else:
dupfont[namestr] = [dupp, s]
else: f_n[namestr] = [s, ti]
#f_n[namestr] = [s, ti]
# if f_n.get(fname) is None: f_n[fname] = [[namestr], s]
# #print(fname, name.toStr(), f_n.get(fname))
# if namestr not in f_n[fname][0]:
# f_n[fname][0] = f_n[fname][0] + [namestr]
tc[0].close()
return f_n
#print(filelist)
#if path.exists(fontspath10): filelist = filelist.extend(os.listdir(fontspath10))
#for s in font_name.keys(): print('{0}: {1}'.format(s, font_name[s]))
# 系统字体完整性检查,检查是否有ASS所需的全部字体,如果没有,则要求拖入
# 需要以下输入
# fontlist: 字体与其所需字符(只读字体部分) { ASS内的字体名称 : 字符串 }
# font_name: 字体名称与字体路径对应词典 { 字体名称 : [ 字体绝对路径, 字体索引 ] }
# 以下输入可选
# assfont: 结构见下,用于多ASS文件时更新列表
# onlycheck: 缺少字体时不要求输入
# 将会返回以下
# assfont: { 字体绝对路径?字体索引 : [ 字符串, ASS内的字体名称 ]}
# font_name: 同上,用于有新字体拖入时对该词典的更新
def checkAssFont(fontlist: dict, font_name: dict, assfont: dict = {}, onlycheck: bool = False):
# 从fontlist获取字体名称
for s in fontlist.keys():
cok = False
# 在全局字体名称词典中寻找字体名称
if s not in font_name:
# 如果找不到,将字体名称统一为小写再次查找
font_name_cache = {}
for ss in font_name.keys():
if ss.lower() == s.lower():
font_name_cache[s] = font_name[ss]
cok = True
break
# update字体名称词典
font_name.update(font_name_cache)
else: cok = True
directout = 0
if not cok:
# 如果 onlycheck 不为 True,向用户要求目标字体
if not onlycheck:
print('\033[1;31m[ERROR] 缺少字体\"{0}\"\n请输入追加的字体文件或其所在字体目录的绝对路径\033[0m'.format(s))
addFont = {}
inFont = ''
while inFont == '' and directout < 3:
inFont = input().strip('\"').strip(' ')
if path.exists(inFont):
if path.isdir(inFont):
addFont = fontProgress(getFileList([inFont], noreg=True)[0])
else:
addFont = fontProgress([[inFont, '0']])
if s not in addFont.keys():
if path.isdir(inFont):
print('\033[1;31m[ERROR] 输入路径中\"{0}\"没有所需字体\"{1}\"\033[0m'.format(inFont, s))
else: print('\033[1;31m[ERROR] 输入字体\"{0}\"不是所需字体\"{1}\"\033[0m'.format('|'.join(addFont.keys()), s))
inFont = ''
else:
font_name.update(addFont)
cok = True
else:
print('\033[1;31m[ERROR] 您没有输入任何字符!再回车{0}次回到主菜单\033[0m'.format(3-directout))
directout += 1
inFont = ''
else:
# 否则直接添加空
assfont['?'.join([s, s])] = ['', s]
if cok and directout < 3:
# 如果找到,添加到assfont列表
font_path = font_name[s][0]
font_index = font_name[s][1]
dict_key = '?'.join([font_path, str(font_index)])
# 如果 assfont 列表已有该字体,则将新字符添加到 assfont 中
if assfont.get(dict_key) is None:
assfont[dict_key] = [fontlist[s], s]
else:
if assfont[dict_key][1] == font_index:
newfname = assfont[dict_key][2]
if s != newfname:
newfname = '|'.join([s, newfname])
newstr = assfont[dict_key][1]
newstr2 = ''
for i in range(0, len(newstr)):
if newstr[i] not in fontlist[s]:
newstr2 = newstr2 + newstr[i]
assfont[dict_key] = [fontlist[s] + newstr2, newfname]
else:
assfont[dict_key] = [fontlist[s], s]
#print(assfont[dict_key])
elif directout >= 3:
return None, font_name
return assfont, font_name
# print('正在输出字体子集字符集')
# for s in fontlist.keys():
# logpath = '{0}_{1}.log'.format(path.join(os.getenv('TEMP'), path.splitext(path.basename(asspath))[0]), s)
# log = open(logpath, mode='w', encoding='utf-8')
# log.write(fontlist[s])
# log.close()
# 字体内部名称变更
def getNameStr(name, subfontcrc: str) -> str:
namestr = ''
nameID = name.nameID
# 变更NameID为1, 3, 4, 6的NameRecord,它们分别对应
# ID Meaning
# 1 Font Family name
# 3 Unique font identifier
# 4 Full font name
# 6 PostScript name for the font
# 注意本脚本并不更改 NameID 为 0 和 7 的版权信息
if nameID in [1,3,4,6]:
namestr = subfontcrc
else:
try:
namestr = name.toStr()
except:
namestr = name.toBytes().decode('utf-16-be', errors='ignore')
return namestr
# 字体子集化
# 需要以下输入:
# assfont: { 字体绝对路径?字体索引 : [ 字符串, ASS内的字体名称 ]}
# fontdir: 新字体存放目录
# 将会返回以下:
# newfont_name: { 原字体名 : [ 新字体绝对路径, 新字体名 ] }
def assFontSubset(assfont: dict, fontdir: str) -> dict:
newfont_name = {}
# print(fontdir)
if path.exists(path.dirname(fontdir)):
if not path.isdir(fontdir):
try:
os.mkdir(fontdir)
except:
print('\033[1;31m[ERROR] 创建文件夹\"{0}\"失败\033[0m'.format(fontdir))
fontdir = os.getcwd()
if not path.isdir(fontdir): fontdir = path.dirname(fontdir)
else: fontdir = os.getcwd()
print('\033[1;33m字体输出路径:\033[0m \033[1m\"{0}\"\033[0m'.format(fontdir))
lk = len(assfont.keys())
kip = 0
for k in assfont.keys():
kip += 1
# 偷懒没有变更该函数中的assfont解析到新的词典格式
# 在这里会将assfont词典转换为旧的assfont列表形式
# assfont: [ 字体绝对路径, 字体索引, 字符串, ASS内的字体名称 ]
s = k.split('?') + [assfont[k][0], assfont[k][1]]
subfontext = ''
fontext = path.splitext(path.basename(s[0]))[1]
if fontext[1:].lower() in ['otc', 'ttc']:
subfontext = fontext[:3] + 'f'
else: subfontext = fontext
#print(fontdir, path.exists(path.dirname(fontdir)), path.exists(fontdir))
fontname = re.sub(cillegal, '_', s[3])
subfontpath = path.join(fontdir, fontname + subfontext)
#print(fontdir, subfontpath)
# if not path.exists(path.dirname(subfontpath)):
# try:
# os.mkdir(path.dirname(subfontpath))
# except:
# subfontpath = path.join(fontdir, fontname + subfontext)
# print('\033[1;31m[ERROR] 创建文件夹\"{0}\"失败\033[0m'.format(fontdir))
subsetarg = [s[0], '--text={0}'.format(s[2]), '--output-file={0}'.format(subfontpath), '--font-number={0}'.format(s[1]), '--passthrough-tables']
print('\r\033[1;32m[{0}/{1}]\033[0m \033[1m正在子集化…… \033[0m'.format(kip, lk), end='')
try:
subset.main(subsetarg)
except PermissionError:
print('\n\033[1;31m[ERROR] 文件\"{0}\"访问失败\033[0m'.format(path.basename(subfontpath)))
continue
except:
# print('\033[1;31m[ERROR] 失败字符串: \"{0}\" \033[0m'.format(s[2]))
print('\n\033[1;31m[ERROR] {0}\033[0m'.format(sys.exc_info()))
print('\033[1;31m[WARNING] 字体\"{0}\"子集化失败,将会保留完整字体\033[0m'.format(path.basename(s[0])))
# crcnewf = ''.join([path.splitext(subfontpath)[0], fontext])
# shutil.copy(s[0], crcnewf)
ttLib.TTFont(s[0], lazy=False, fontNumber=int(s[1])).save(subfontpath, False)
subfontcrc = None
# newfont_name[s[3]] = [crcnewf, subfontcrc]
newfont_name[s[3]] = [subfontpath, subfontcrc]
continue
#os.system('pyftsubset {0}'.format(' '.join(subsetarg)))
if path.exists(subfontpath):
subfontbyte = open(subfontpath, mode='rb')
subfontcrc = str(hex(zlib.crc32(subfontbyte.read())))[2:].upper()
if len(subfontcrc) < 8: subfontcrc = '0' + subfontcrc
# print('CRC32: {0} \"{1}\"'.format(subfontcrc, path.basename(s[0])))
subfontbyte.close()
rawf = ttLib.TTFont(s[0], lazy=True, fontNumber=int(s[1]))
newf = ttLib.TTFont(subfontpath, lazy=False)
if len(newf['name'].names) == 0:
for i in range(0,7):
if len(rawf['name'].names) - 1 >= i:
name = rawf['name'].names[i]
namestr = getNameStr(name, subfontcrc)
newf['name'].addName(namestr, minNameID=-1)
else:
for i in range(0, len(rawf['name'].names)):
name = rawf['name'].names[i]
nameID = name.nameID
platID = name.platformID
langID = name.langID
platEncID = name.platEncID
namestr = getNameStr(name, subfontcrc)
newf['name'].setName(namestr ,nameID, platID, platEncID, langID)
if len(newf.getGlyphOrder()) == 1 and '.notdef' in newf.getGlyphOrder():
print('\n\033[1;31m[WARNING] 字体\"{0}\"子集化失败,将会保留完整字体\033[0m'.format(path.basename(s[0])))
crcnewf = subfontpath
newf.close()
if not subfontpath == s[0]: os.remove(subfontpath)
# shutil.copy(s[0], crcnewf)
rawf.save(crcnewf, False)
subfontcrc = None
else:
crcnewf = '.{0}'.format(subfontcrc).join(path.splitext(subfontpath))
newf.save(crcnewf)
newf.close()
rawf.close()
if path.exists(crcnewf):
if not subfontpath == crcnewf: os.remove(subfontpath)
newfont_name[s[3]] = [crcnewf, subfontcrc]
print('')
#print(newfont_name)
return newfont_name
# 更改ASS样式对应的字体
# 需要以下输入
# fullass: 完整的ass文件内容,以行分割为列表
# newfont_name: { 原字体名 : [ 新字体路径, 新字体名 ] }
# asspath: 原ass文件的绝对路径
# styleline: [V4/V4+ Styles]标签在SSA/ASS中的行数,对应到fullass列表的索引数
# font_pos: Font参数在 Styles 的 Format 中位于第几个逗号之后
# 以下输入可选
# outdir: 新字幕的输出目录,默认为源文件目录
# ncover: 为True时不覆盖原有文件,为False时覆盖
# fn_lines: 带有fn标签的行数,对应到fullass的索引
# 将会返回以下
# newasspath: 新ass文件的绝对路径
def assFontChange(fullass: list, newfont_name: dict, asspath: str, styleline: int,
font_pos: int, outdir: str = '', ncover: bool = False, fn_lines: list = []) -> str:
# 扫描Style各行,并替换掉字体名称
#print('正在替换style对应字体......')
for i in range(styleline + 2, len(fullass)):
if len(fullass[i].split(':')) < 2:
if re.search(style_read, '\n'.join(fullass[i + 1:])) is None:
break
else:
continue
styleStr = ''.join(fullass[i].split(':')[1:]).strip(' ').split(',')
fontstr = styleStr[font_pos].lstrip('@')
if not newfont_name.get(fontstr) is None:
if not newfont_name[fontstr][1] is None:
fullass[i] = fullass[i].replace(fontstr, newfont_name[fontstr][1])
if len(fn_lines) > 0:
#print('正在处理fn标签......')
for fl in fn_lines:
fn_line = fullass[fl[0]]
for ti in range(1, len(fl)):
for k in newfont_name.keys():
if k in fl[ti]:
fn_line = fn_line.replace(fl[ti], fl[ti].replace(k, newfont_name[k][1]))
fullass[fl[0]] = fn_line
if path.exists(path.dirname(outdir)):
if not path.isdir(outdir):
try:
os.mkdir(outdir)
except:
print('\033[1;31m[ERROR] 创建文件夹\"{0}\"失败\033[0m'.format(outdir))
outdir = os.getcwd()
print('\033[1;33m字幕输出路径:\033[0m \033[1m\"{0}\"\033[0m'.format(outdir))
if path.isdir(outdir):
newasspath = path.join(outdir, '.subset'.join(path.splitext(path.basename(asspath))))
else: newasspath = '.subset'.join(path.splitext(asspath))
if path.exists(newasspath) and ncover:
testpathl = path.splitext(newasspath)
testc = 1
testpath = '{0}#{1}{2}'.format(testpathl[0], testc, testpathl[1])
while path.exists(testpath):
testc += 1
testpath = '{0}#{1}{2}'.format(testpathl[0], testc, testpathl[1])
newasspath = testpath
ass = open(newasspath, mode='w', encoding='utf-8')
ass.writelines(fullass)
ass.close()
#print('ASS样式转换完成: {0}'.format(path.basename(newasspath)))
return newasspath
# ASFMKV,将媒体文件、字幕、字体封装到一个MKV文件,需要mkvmerge命令行支持
# 需要以下输入
# file: 媒体文件绝对路径
# outfile: 输出文件的绝对路径,如果该选项空缺,默认为 输入媒体文件.muxed.mkv
# asslangs: 赋值给字幕轨道的语言,如果字幕轨道多于asslangs的项目数,超出部分将全部应用asslangs的末项
# asspaths: 字幕绝对路径列表
# fontpaths: 字体列表,格式为 [[字体1绝对路径], [字体1绝对路径], ...],必须嵌套一层,因为主函数偷懒了
# 将会返回以下
# mkvmr: mkvmerge命令行的返回值
def ASFMKV(file: str, outfile: str = '', asslangs: list = [], asspaths: list = [], fontpaths: list = []) -> int:
#print(fontpaths)
global rmAssIn, rmAttach, mkvout, notfont
if file is None: return 4
elif file == '': return 4
elif not path.exists(file) or not path.isfile(file): return 4
if outfile is None: outfile = ''
if outfile == '' or not path.exists(path.dirname(outfile)) or path.dirname(outfile) == path.dirname(file):
outfile = '.muxed'.join([path.splitext(file)[0], '.mkv'])
outfile = path.splitext(outfile)[0] + '.mkv'
if path.exists(outfile):
checkloop = 1
while path.exists('#{0}'.format(checkloop).join(path.splitext(outfile))):
checkloop += 1
outfile = '#{0}'.format(checkloop).join([path.splitext(outfile)[0], '.mkv'])
mkvargs = []
if rmAssIn: mkvargs.append('-S')
if rmAttach: mkvargs.append('-M')
mkvargs.extend(['(', file, ')'])
fn = path.splitext(path.basename(file))[0]
if len(asspaths) > 0:
for i in range(0, len(asspaths)):
s = asspaths[i]
assfn = path.splitext(path.basename(s))[0]
assnote = assfn[(assfn.find(fn) + len(fn)):].replace('.subset', '')
#print(assfn, fn, assnote)
if len(assnote) > 1:
mkvargs.extend(['--track-name', '0:{0}'.format(assnote.lstrip('.'))])
if len(asslangs) > 0 and path.splitext(s)[1][1:].lower() not in ['idx']:
mkvargs.append('--language')
if i < len(asslangs):
mkvargs.append('0:{0}'.format(asslangs[i]))
else:
mkvargs.append('0:{0}'.format(asslangs[len(asslangs) - 1]))
mkvargs.extend(['(', s, ')'])
if len(fontpaths) > 0:
for s in fontpaths:
fext = path.splitext(s[0])[1][1:].lower()
if fext in ['ttf', 'ttc']:
mkvargs.extend(['--attachment-mime-type', 'application/x-truetype-font'])
elif fext in ['otf', 'otc']:
mkvargs.extend(['--attachment-mime-type', 'application/vnd.ms-opentype'])
mkvargs.extend(['--attach-file', s[0]])
mkvargs.extend(['--title', fn])
mkvjsonp = path.splitext(file)[0] + '.mkvmerge.json'
mkvjson = open(mkvjsonp, mode='w', encoding='utf-8')
json.dump(mkvargs, fp=mkvjson, sort_keys=True, indent=2, separators=(',', ': '))
mkvjson.close()
mkvmr = os.system('mkvmerge @\"{0}\" -o \"{1}\"'.format(mkvjsonp, outfile))
if mkvmr > 1:
print('\n\033[1;31m[ERROR] 检测到不正常的mkvmerge返回值,重定向输出...\033[0m')
os.system('mkvmerge -r \"{0}\" @\"{1}\" -o NUL'.format('{0}.{1}.log'
.format(path.splitext(file)[0], datetime.now().strftime('%Y-%m%d-%H%M-%S_%f')), mkvjsonp))
elif not notfont:
for p in asspaths:
print('\033[1;32m封装成功: \033[1;37m\"{0}\"\033[0m'.format(p))
if path.splitext(p)[1][1:].lower() in ['ass', 'ssa']:
try:
os.remove(p)
except:
print('\033[1;33m[ERROR] 文件\"{0}\"删除失败\033[0m'.format(p))
for f in fontpaths:
print('\033[1;32m封装成功: \033[1;37m\"{0}\"\033[0m'.format(f[0]))
try:
os.remove(f[0])
except:
print('\033[1;33m[ERROR] 文件\"{0}\"删除失败\033[0m'.format(f[0]))
try:
os.remove(mkvjsonp)
except:
print('\033[1;33m[ERROR] 文件\"{0}\"删除失败\033[0m'.format(mkvjsonp))
print('\033[1;32m输出成功:\033[0m \033[1m\"{0}\"\033[0m'.format(outfile))
else:
print('\033[1;32m输出成功:\033[0m \033[1m\"{0}\"\033[0m'.format(outfile))
return mkvmr
# 从输入的目录中获取媒体文件列表
# 需要以下输入
# dir: 要搜索的目录
# 返回以下结果
# medias: 多媒体文件列表
# 结构: [[ 文件名(无扩展名), 绝对路径 ], ...]
def getMediaFilelist(dir: str) -> list:
medias = []
global v_subdir, extlist
if path.isdir(dir):
if v_subdir:
for r,ds,fs in os.walk(dir):
for f in fs:
if path.splitext(f)[1][1:].lower() in extlist:
medias.append([path.splitext(path.basename(f))[0], path.join(r, f)])
else:
for f in os.listdir(dir):
if path.isfile(path.join(dir, f)):
if path.splitext(f)[1][1:].lower() in extlist:
medias.append([path.splitext(path.basename(f))[0], path.join(dir, f)])
return medias
# 在目录中找到与媒体文件列表中的媒体文件对应的字幕
# 遵循以下原则
# 媒体文件在上级目录,则匹配子目录中的字幕;媒体文件的字幕只能在媒体文件的同一目录或子目录中,不能在上级目录和其他同级目录
# 需要以下输入
# medias: 媒体文件列表,结构见 getMediaFilelist
# cpath: 开始搜索的顶级目录
# 将会返回以下
# media_ass: 媒体文件与字幕文件的对应词典
# 结构: { 媒体文件绝对路径 : [ 字幕1绝对路径, 字幕2绝对路径, ...] }
def getSubtitles(medias: list, cpath: str) -> dict:
media_ass = {}
global s_subdir, matchStrict
if s_subdir:
for r,ds,fs in os.walk(cpath):
for f in [path.join(r, s) for s in fs if path.splitext(s)[1][1:].lower() in subext]:
if '.subset' in path.basename(f): continue
for l in medias:
vdir = path.dirname(l[1])
sdir = path.dirname(f)
sext = path.splitext(f)
if (l[0] in f and not matchStrict) or (l[0] == path.basename(f)[:len(l[0])] and matchStrict):
if((vdir in sdir and sdir not in vdir) or (vdir == sdir)):
if sext[1][:1].lower() == 'idx':
if not path.exists(sext[1] + '.sub'):
continue
if media_ass.get(l[1]) is None:
media_ass[l[1]] = [f]
else: media_ass[l[1]].append(f)
else:
for f in [path.join(cpath, s) for s in os.listdir(cpath) if not path.isdir(s) and
path.splitext(s)[1][1:].lower() in subext]:
# print(f, cpath)
if '.subset' in path.basename(f): continue
for l in medias:
# print(path.basename(f)[len(l[0]):], l)
sext = path.splitext(f)
if (l[0] in f and not matchStrict) or (l[0] == path.basename(f)[:len(l[0])] and matchStrict):
if path.dirname(l[1]) == path.dirname(f):
if sext[1][:1].lower() == 'idx':
if not path.exists(sext[1] + '.sub'):
continue
if media_ass.get(l[1]) is None:
media_ass[l[1]] = [f]
else: media_ass[l[1]].append(f)
return media_ass
# 主函数,负责调用各函数走完完整的处理流程
# 需要以下输入
# font_name: 字体名称与字体路径对应词典,结构见 fontProgress
# asspath: 字幕绝对路径列表
# 以下输入可选
# outdir: 输出目录,格式 [ 字幕输出目录, 字体输出目录, 视频输出目录 ],如果项数不足,则取最后一项;默认为 asspaths 中每项所在目录
# mux: 不要封装,只运行到子集化完成
# vpath: 视频路径,只在 mux = True 时生效
# asslangs: 字幕语言列表,将会按照顺序赋给对应的字幕轨道,只在 mux = True 时生效
# 将会返回以下
# newasspath: 列表,新生成的字幕文件绝对路径
# newfont_name: 词典,{ 原字体名 : [ 新字体绝对路径, 新字体名 ] }
# ??? : 数值,mkvmerge的返回值;如果 mux = False,返回-1
def main(font_name: dict, asspath: list, outdir: list = ['', '', ''], mux: bool = False, vpath: str = '', asslangs: list = []):
print('')
outdir_temp = outdir[:3]
outdir = ['', '', '']
for i in range(0, len(outdir_temp)):
s = outdir_temp[i]
# print(s)
if s is None:
outdir[i] = ''
elif s == '':
outdir[i] = s
else:
try:
if not path.isdir(s) and path.exists(path.dirname(s)):
os.mkdir(s)
if path.isdir(s): outdir[i] = s
except:
print('\033[1;31m[ERROR] 创建输出文件夹错误\n[ERROR] {0}\033[0m'.format(sys.exc_info()))
if '\\' in s:
outdir[i] = path.join(os.getcwd(), path.basename(s.rstrip('\\')))
else: outdir[i] = path.join(os.getcwd(), s)
# print(outdir)
# os.system('pause')
global notfont
# multiAss 多ASS文件输入记录词典
# 结构: { ASS文件绝对路径 : [ 完整ASS文件内容(fullass), 样式位置(styleline), 字体在样式行中的位置(font_pos) ]}
multiAss = {}
assfont = {}
fontlist = {}
newasspath = []
fo = ''
if not notfont:
# print('\n字体名称总数: {0}'.format(len(font_name.keys())))
# noass = False
for i in range(0, len(asspath)):
s = asspath[i]
if path.splitext(s)[1][1:].lower() not in ['ass', 'ssa']:
multiAss[s] = [[], 0, 0]
else:
# print('正在分析字幕文件: \"{0}\"'.format(path.basename(s)))
fullass, fontlist, styleline, font_pos, fn_lines = assAnalyze(s, fontlist)
multiAss[s] = [fullass, styleline, font_pos]