-
Notifications
You must be signed in to change notification settings - Fork 1
/
ASFMKV_py1.02-pre6.py
2007 lines (1950 loc) · 93.7 KB
/
ASFMKV_py1.02-pre6.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'
# *************************************************************************
# no_extcheck 关闭对extlist的扩展名检查来添加一些可能支持的格式
no_extcheck = False
# *************************************************************************
# 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
# *************************************************************************
# warningStop 对有可能子集化失败的字体的处理方式
# True 不子集化
# False 子集化(如果失败,会保留原始字体;如果原始字体是TTC/OTC,则会保留其中的TTF/OTF)
warningStop = False
# errorStop 子集化失败后的处理方式
# True 终止批量子集化
# False 继续运行(运行方式参考warningStop)
errorStop = 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']
# lcidfil
# LCID过滤器,用于选择字体名称所用语言
# 结构为:{ platformID(Decimal) : { LCID : textcoding } }
# 目前Textcoding没有实际作用,仅用于让这个词典可读性更强
# 详情可在 https://docs.microsoft.com/en-us/typography/opentype/spec/name#platform-encoding-and-language-ids 查询
lcidfil = {
3: {
2052 : 'gbk',
1042 : 'euc-kr',
1041 : 'shift-jis',
1033 : 'utf-16be',
1028 : 'big5',
0 : 'utf-16be'
},
2: {
0 : 'ascii',
1 : 'utf-8',
2 : 'iso-8859-1'
},
1: {
33 : 'gbk',
23 : 'euc-kr',
19 : 'big5',
11 : 'shift-jis',
0 : 'mac-roman'
},
0 : {
0 : 'utf-16be',
1 : 'utf-16be',
2 : 'utf-16be',
3 : 'utf-16be',
4 : 'utf-16be',
5 : 'utf-16be'
}}
# 以下环境变量不应更改
# 编译 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('@')
# print(fn, s)
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
def updateAssFont(assfont: dict = {}, assfont2: dict = {}):
for key in assfont2.keys():
if assfont.get(key) is not None:
assfont[key] = [
assfont[key][0] + ''.join([ks for ks in assfont2[key][0] if ks not in assfont[key][0]]),
assfont[key][1] + ''.join(['|' + ks for ks in assfont2[key][1].split('|') if ks.lower() not in assfont[key][1].lower()]),
assfont[key][2] + ''.join(['|' + ks for ks in assfont2[key][2].split('|') if ks.lower() not in assfont[key][2].lower()])
]
else:
assfont[key] = assfont2[key]
del assfont2
return assfont
# ASS分析部分
# 需要输入
# asspath: ASS文件的绝对路径
# font_name: 字体名称与字体绝对路径词典,用于查询 Bold、Italic 字体
# 可选输入
# fontlist: 可以更新fontlist,用于多ASS同时输入的情况,结构见下
# onlycheck: 只确认字幕中的字体,仅仅返回fontlist
# 将会返回
# fullass: 完整的ASS文件内容,以行分割
# fontlist: 字体与其所需字符 { 字体 : 字符串 }
# styleline: 样式内容的起始行
# font_pos: 字体在样式中的位置
# fn_lines: 带有fn标签的行数与该行的完整特效标签,一项一个 [ [行数, { 标签1 : ( ASS内部字体名称, Italic, Bold )}], ... ]
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
bold_pos = 0
italic_pos = 0
infoline = 0
styleline = 0
ssRecover = {}
fn_lines = []
# 编译分析用正则表达式
style = re.compile(r'^\[[Vv]4.*[Ss]tyles\]$')
event = re.compile(r'^\[[Ee]vents\]$')
sinfo = re.compile(r'^\[[Ss]cript [Ii]nfo\]$')
# 识别文本编码并读取整个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标签、Script Info标签来确认起始行
for s in range(0, asslen):
if re.match(sinfo, fullass[s]) is not None:
infoline = s
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
# 子集化字体信息还原
# 如果在 [Script Info] 中找到字体子集化的注释信息,则将字体名称还原并移除注释
ssRemove = []
for s in range(infoline + 1, asslen):
ss = fullass[s]
if ss.strip(' ') == '':
continue
elif ss.strip(' ')[0] == ';' and ss.lower().find('font subset') > -1 and len(ss.split(':')) > 1:
fr = ss.split(':')[1].split('-')
ssRecover[fr[0].strip(' ').rstrip('\n')] = '-'.join(fr[1:]).strip(' ').rstrip('\n')
ssRemove.append(s)
elif ss.strip(' ')[0] == '[' and ss.strip(' ')[-1] == ']':
break
else: continue
if len(ssRemove) > 0:
ssRemove.reverse()
for s in ssRemove:
fullass.pop(s)
asslen -= 1
infoline -= 1
styleline -= 1
eventline -= 1
del ssRemove
# 获取Style的 Format 行,并用半角逗号分割
style_format = ''.join(fullass[styleline + 1].split(':')[1:]).strip(' ').split(',')
# 确定Style中 Name 、 Fontname 、 Bold 、 Italic 的位置
style_pos, font_pos, bold_pos, italic_pos = -1, -1, -1, -1
for i in range(0, len(style_format)):
s = style_format[i].lower().strip(' ').replace('\n', '')
if s == 'name':
style_pos = i
elif s == 'fontname':
font_pos = i
elif s == 'bold':
bold_pos = i
elif s == 'italic':
italic_pos = i
if style_pos != -1 and font_pos != -1 and bold_pos != -1 and italic_pos != -1:
break
if style_pos == font_pos == bold_pos == italic_pos == -1:
style_pos, font_pos, bold_pos, italic_pos = 0, 0, 0, 0
# 获取 字体表 与 样式字体对应表
style_font = {}
# style_font 词典内容:
# { 样式 : 字体名 }
# fontlist 词典内容:
# { 字体名?斜体?粗体 : 使用该字体的文本 }
for i in range(styleline + 2, asslen):
# 如果该行不是样式行,则跳过,直到文件末尾或出现其他标记
if len(fullass[i].split(':')) < 2:
continue
if fullass[i].split(':')[0].strip(' ').lower() != 'style':
break
styleStr = ''.join([s.strip(' ') for s in fullass[i].split(':')[1:]]).strip(' ').split(',')
font_key = styleStr[font_pos].lstrip('@')
# 如果有字体还原信息,则将样式行对应的字体还原
if len(ssRecover) > 0:
if ssRecover.get(font_key) is not None:
font_key = ssRecover[font_key]
styleStr[font_pos] = font_key
fullass[i] = fullass[i].split(':')[0] + ': ' + ','.join(styleStr)
# 获取样式行中的粗体斜体信息
isItalic = - int(styleStr[italic_pos])
isBold = - int(styleStr[bold_pos])
fontlist.setdefault('{0}?{1}?{2}'.format(font_key, isItalic, isBold), '')
style_font[styleStr[style_pos]] = '{0}?{1}?{2}'.format(font_key, isItalic, isBold)
#print(fontlist)
# 提取Event的 Format 行,并用半角逗号分割
event_format = ''.join(fullass[eventline + 1].split(':')[1:]).strip(' ').split(',')
style_pos2, text_pos = -1, -1
# 确定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 != -1 and text_pos != -1:
break
if style_pos2 == -1 and text_pos == -1:
style_pos2, text_pos == 0, 0
# 获取 字体的字符集
# 先获取 Style,用style_font词典查找对应的 Font
# 再将字符串追加到 fontlist 中对应 Font 的值中
for i in range(eventline + 2, asslen):
eventline_sp = fullass[i].split(':')
if len(eventline_sp) < 2:
continue
#print(fullass[i])
if eventline_sp[0].strip(' ').lower() == 'comment':
continue
elif eventline_sp[0].strip(' ').lower() != 'dialogue':
break
eventline_sp = ''.join(eventline_sp[1:]).split(',')
eventftext = ','.join(eventline_sp[text_pos:])
effectDel = r'(\{.*?\})|(\\[hnN])|(\s)'
textremain = ''
# 矢量绘图处理,如果发现有矢量表达,从字符串中删除这一部分
# 这一部分的工作未经详细验证,作用也不大
if re.search(r'\{.*?\\p[1-9][0-9]*.*?\}([\s\S]*?)\{.*?\\p0.*?\}', eventftext) is not None:
vecpos = re.findall(r'\{.*?\\p[1-9][0-9]*.*?\}[\s\S]*?\{.*?\\p0.*?\}', eventftext)
vecfind = 0
nexts = 0
for s in vecpos:
vecfind = eventftext.find(s)
textremain += eventftext[nexts:vecfind]
nexts = vecfind
s = re.sub(r'\\p[0-9]+', '', re.sub(r'}.*?{', '}{', s))
textremain += s
elif re.search(r'\{.*?\\p[1-9][0-9]*.*?\}', eventftext) is not None:
eventftext = re.sub(r'\\p[0-9]+', '', eventftext[:re.search(r'\{.*?\\p[1-9][0-9]*.*?\}', eventftext).span()[0]])
if len(textremain) > 0:
eventftext = textremain
eventfont = style_font.get(eventline_sp[style_pos2].lstrip('*'))
# 粗体、斜体标签处理
splittext = []
splitpos = []
# 首先查找蕴含有启用粗体/斜体标记的特效标签
if re.search(r'\{.*?(?:\\b[7-9]00|\\b1|\\i1).*?\}', eventftext) is not None:
lastfind = 0
allfind = re.findall(r'\{.*?\}', eventftext)
# 在所有特效标签中寻找
# 启用粗体斜体
# 禁用粗体斜体
# 分成两种情况再进入下一层嵌套if
# 启用粗体/启用斜体
# 禁用粗体/禁用斜体
# 然后分别确认该特效标签的适用范围,以准确将字体子集化
for sti in range(0, len(allfind)):
st = allfind[sti]
ibclose = re.search(r'(\\b[1-4]00|\\b0|\\i0|\\b[\\\}]|\\i[\\\}])', st)
ibopen = re.search(r'(\\b[7-9]00|\\b1|\\i1)', st)
if ibopen is not None:
stfind = eventftext.find(st)
for stii in range(sti, -1, -1):
if len(splitpos) > 0:
if splitpos[-1][1] >= eventftext.find(allfind[stii]):
break
if allfind[stii].find('\\fn') > -1:
stfind = eventftext.find(allfind[stii])
break
addbold = '0'
additalic = '0'
if re.search(r'(\\b[7-9]00|\\b1)', st) is not None:
if re.search(r'(\\b[7-9]00|\\b1)', st).span()[0] > max([st.find('\\b0'), st.find('\\b\\'), st.find('\\b}')]):
addbold = '1'
if st.find('\\i1') > st.find('\\i0'): additalic = '1'
if len(splittext) == 0:
if stfind > 0:
splittext.append([eventftext[:stfind], '0', '0'])
splittext.append([eventftext[stfind:], additalic, addbold])
else:
if splittext[-1][1] != additalic or splittext[-1][2] != addbold:
if stfind > 0:
splittext[-1][0] = eventftext[lastfind:stfind]
splittext.append([eventftext[stfind:], additalic, addbold])
lastfind = stfind
elif ibclose is not None:
stfind = eventftext.find(st)
for stii in range(sti, -1, -1):
if len(splitpos) > 0:
if splitpos[-1][1] >= eventftext.find(allfind[stii]):
break
if allfind[stii].find('\\fn') > -1:
stfind = eventftext.find(allfind[stii])
break
if len(splittext) > 0:
ltext = splittext[-1]
readytext = [eventftext[stfind:], ltext[1], ltext[2]]
if re.search(r'(\\i0|\\i[\\\}])', st) is not None:
readytext[1] = '0'
if re.search(r'(\\b[1-4]00|\\b0|\\b[\\\}])', st) is not None:
readytext[2] = '0'
if ltext[1] != readytext[1] or ltext[2] != readytext[2]:
if stfind > 0:
splittext[-1][0] = eventftext[lastfind:stfind]
splittext.append(readytext)
lastfind = stfind
else:
splittext.append([eventftext, '0', '0'])
else: continue
if len(splittext) == 0:
splittext.append([eventftext, '0', '0'])
# elif len(splittext) > 1:
# print(splittext)
splitpos = [[0, len(splittext[0][0]), int(splittext[0][1]), int(splittext[0][2])]]
if len(splittext) > 1:
for spi in range(1, len(splittext)):
spil = len(splittext[spi][0])
lspil = splitpos[spi - 1][1]
splitpos.append([lspil, spil + lspil, int(splittext[spi][1]), int(splittext[spi][2])])
# print(splitpos)
# print(eventftext)
# os.system('pause')
del splittext
# print(eventftext)
# 字体标签分析
eventftext2 = eventftext
# 在全部特效标签中寻找 fn 字体标签
itl = [its for its in re.findall(r'\{.*?\}', eventftext) if its.find('\\fn') > -1]
# 如果有发现 fn 字体标签,利用 while 循环确定该标签的适用范围和所包含的文本
if len(itl) > 0:
fn = ''
fn_line = [i]
newstrl = []
it = (0, 0)
while len(itl) > 0:
if len(newstrl) == 0:
newstrl.append([eventftext, 0, ''])
itpos = eventftext.find(itl[0])
it = (itpos, itpos + len(itl[0]))
s = eventftext[(it[0] + 1):(it[1] - 1)]
cuttext = eventftext[it[0]:]
fnpos = eventftext2.find(cuttext)
itl = itl[1:]
if len(itl) > 0:
itpos = eventftext.find(itl[0])
newstrl[-1][0] = newstrl[-1][0][:itpos]
else:
newstrl[-1][0] = newstrl[-1][0][:it[0]]
newstrl.append([cuttext, fnpos, s])
eventftext = eventftext[it[0]:]
newstrl = [nst for nst in newstrl if len(nst[0].strip(' ')) > 0]
# 然后将新增的字体所对应的文本添加到fn_line中
fn = ''
for sp in splitpos:
for fi in range(0, len(newstrl)):
fs = newstrl[fi]
s = fs[2]
if int(fs[1]) in range(sp[0], sp[1]):
fss = re.sub(effectDel, '', fs[0])
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('@')
if len(ssRecover) > 0:
if ssRecover.get(fn) is not None:
fullassSplit = fullass[i].split(',')
fullass[i] = ','.join(fullassSplit[0:text_pos] + [','.join(fullassSplit[text_pos:]).replace(fn, ssRecover[fn])])
s = s.replace(fn, ssRecover[fn])
fn = ssRecover[fn]
fn_line.append({s : (fn, str(sp[2]), str(sp[3]))})
break
if len(fn) == 0:
fontlist = fontlistAdd(fss, eventfont, fontlist)
else:
fontlist = fontlistAdd(fss, '?'.join([fn, str(sp[2]), str(sp[3])]), fontlist)
# s = re.sub(effectDel, '', eventftext[:it[0]])
# print('add', s)
# print('fn', fn)
# print('ef', eventftext)
# it = re.search(r'\{\\fn.*?\}', eventftext)
# os.system('pause')
fn_lines.append(fn_line)
del fn_line
else:
if not eventfont is None:
# 去除行中非文本部分,包括特效标签{},硬软换行符
eventtext = re.sub(effectDel, '', eventftext)
fontlist = fontlistAdd(eventtext, eventfont, fontlist)
fl_popkey = []
# 在字体列表中检查是否有没有在文本中使用的字体,如果有,添加到删去列表
for s in fontlist.keys():
if len(fontlist[s]) == 0:
fl_popkey.append(s)
#print('跳过没有字符的字体\"{0}\"'.format(s))
# 删去 删去列表 中的字体
if len(fl_popkey) > 0:
for s in fl_popkey:
fontlist.pop(s)
# print(fontlist)
# 如果含有字体恢复信息,则恢复字幕
if len(ssRecover) > 0:
try:
nsdir = path.join(path.dirname(asspath), 'NoSubsetSub')
#nsfile = path.join(nsdir, '.NoSubset'.join(path.splitext(path.basename(asspath))))
nsfile = path.join(nsdir, path.basename(asspath))
if not path.exists(nsdir): os.mkdir(nsdir)
recoverass = open(nsfile, mode='w', encoding='utf-8')
recoverass.writelines(fullass)
recoverass.close()
print('\033[1;33m已恢复字幕: \033[0m\033[1m\"{0}\"\033[0m'.format(nsfile))
except:
print('\033[1;31m[ERROR] 恢复的子集化字幕写入失败\n{0}\033[0m'.format(sys.exc_info()))
# 如果 onlycheck 为 True,只返回字体列表
if onlycheck:
del fullass
style_font.clear()
return None, fontlist, None, None, None, None
return fullass, fontlist, styleline, font_pos, fn_lines, infoline
# 获取字体文件列表
# 接受输入
# 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
def fnReadCorrect(ttfont: ttLib.ttFont, index: int, fontpath: str) -> str:
name = ttfont['name'].names[index]
namestr = name.toBytes().decode('utf-16-be', errors='ignore')
try:
# 尝试使用 去除 \x00 字符 解码
print('')
if len([i for i in name.toBytes() if i == 0]) > 0:
nnames = ttfont['name']
namebyteL = name.toBytes()
for bi in range(0, len(namebyteL), 2):
if namebyteL[bi] != 0:
print('\033[1;33m尝试修正字体\"{0}\"名称读取 >> \"{1}\"\033[0m'.format(path.basename(fontpath), namestr))
return namestr
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[index].toStr()
print('\033[1;33m已修正字体\"{0}\"名称读取 >> \"{1}\"\033[0m'.format(path.basename(fontpath), namestr))
#os.system('pause')
else: namestr = name.toBytes().decode('utf-16-be', errors='ignore')
# 如果没有 \x00 字符,使用 utf-16-be 强行解码;如果有,尝试解码;如果解码失败,使用 utf-16-be 强行解码
except:
print('\033[1;33m尝试修正字体\"{0}\"名称读取 >> \"{1}\"\033[0m'.format(path.basename(fontpath), namestr))
return namestr
def outputSameLength(s: str) -> str:
length = 0
output = ''
for i in range(len(s) -1, -1, -1):
si = s[i]
if 65281 <= ord(si) <= 65374 or ord(si) == 12288 or ord(si) not in range(33, 127):
length += 2
else: length += 1
if length >= 56:
output = '..' + output
break
else: output = si + output
return output + ''.join([' ' for ss in range(0, 60 - length)])
#字体处理部分
# 需要输入
# fl: 字体文件列表
# 可选输入
# f_n: 默认新建一个,可用于更新font_name
# 将会返回
# font_name: 字体内部名称与绝对路径的索引词典
# 会对以下全局变量进行变更
# dupfont: 重复字体的名称与其路径词典
# font_n_lower: 字体全小写名称与其标准名称对应词典
# font_info 列表结构
# [ font_name, font_n_lower, font_family, warning_font ]
# font_name 词典结构
# { 字体名称 : [ 字体绝对路径 , 字体索引 (仅用于TTC/OTC; 如果是TTF/OTF,默认为0) , 字体样式 ] }
# dupfont 词典结构
# { 重复字体名称 : [ 字体1绝对路径, 字体2绝对路径, ... ] }
def fontProgress(fl: list, font_info: list = [{}, {}, {}, []]) -> list:
global dupfont
warning_font = font_info[3]
font_family = font_info[2]
font_n_lower = font_info[1]
f_n = font_info[0]
#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 \033[1m{3}\033[0m'.format(si + 1, flL, ((si + 1)/flL)*100, outputSameLength(path.dirname(s))), end='', flush=True)
isTc = False
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))
isTc = True
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)
isTc = 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]
# 读取字体的 'OS/2' 表的 'fsSelection' 项查询字体的粗体斜体信息
try:
os_2 = bin(t['OS/2'].fsSelection)[2:].zfill(10)
except:
os_2 = '1'.zfill(10)
isItalic = int(os_2[-1])
isBold = int(os_2[-6])
# isCustom = int(os_2[-7])
# isRegular = int(os_2[-7])
# 读取字体的 'name' 表
fstyle = ''
familyN = ''
try:
indexs = len(t['name'].names)
except:
break
isWarningFont = False
namestrl1 = []
namestrl2 = []
donotStyle = False
for ii in range(0, indexs):
name = t['name'].names[ii]
# 若 nameID 为 1,读取 NameRecord 的字体家族名称
if name.nameID == 1:
try:
familyN = name.toStr()
except:
familyN = fnReadCorrect(t, ii, s)
familyN = familyN.strip(' ')
font_family.setdefault(familyN, {})
# 若 nameID 为 2,读取 NameRecord 的字体样式
elif name.nameID == 2 and not donotStyle:
try:
fstyle = name.toStr()
except:
fstyle = fnReadCorrect(t, ii, s)
# 若 nameID 为 4,读取 NameRecord 的字体完整名称
elif name.nameID == 4:
namestr = ''
try:
# if name.isUnicode() or name.platformID == 0:
namestr = name.toStr()
# else:
# errnum = 0
# lcidcache = [lcc for lcc in lcidfil[name.platformID].values() if re.search(r'(unicode)|(utf)|(roman)', lcc.lower()) is None]
# for lcid in lcidcache:
# try:
# namestr = name.toBytes().decode(lcid)
# except:
# errnum += 1
# if errnum >= len(lcidcache):
# namestr = fnReadCorrect(t, ii, s)
# isWarningFont = True
except:
# 如果 fontTools 解码失败,则尝试使用 utf-16-be 直接解码
namestr = fnReadCorrect(t, ii, s)
isWarningFont = True
if namestr is None: continue
if name.langID in lcidfil[name.platformID].keys():
if namestr.strip(' ') not in namestrl1: namestrl1.append(namestr.strip(' '))
if namestr.strip(' ').lower().find(familyN.lower()) > -1: donotStyle = True
else:
if namestr.strip(' ') not in namestrl2: namestrl2.append(namestr.strip(' '))
del namestr
#print(namestrl)
if len(namestrl1) > 0:
namestrl = namestrl1
elif len(namestrl2) > 0:
namestrl = namestrl2
else:
continue
del namestrl1, namestrl2
addfmn = True
for ns in namestrl:
if ns.lower().find(familyN.lower()) > -1:
addfmn = False
if addfmn: namestrl.append(familyN.strip(' '))
#print(namestr, path.basename(s))
for namestr in namestrl:
#print(namestr)
if isWarningFont:
if namestr not in warning_font:
warning_font.append(namestr.lower())
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 and f_n[namestr][2].lower() == fstyle.lower()):
print('\n\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, fstyle, isTc]
font_n_lower[namestr.lower()] = namestr
# print(f_n[namestr], namestr)
if len(familyN) > 0 and namestr != namestrl[-1]:
if font_family[familyN].get((isItalic, isBold)) is not None:
if not namestr in font_family[familyN][(isItalic, isBold)]:
font_family[familyN][(isItalic, isBold)].append(namestr)
else: font_family[familyN].setdefault((isItalic, isBold), [namestr])
tc[0].close()
keys = list(font_family.keys())
for k in keys:
if not len(font_family[k]) > 1:
font_family.pop(k)
del keys
dupfont_cache = dupfont
dupfont = {}
for k in dupfont_cache.keys():
k2 = tuple(dupfont_cache[k])
if dupfont.get(k2) is not None:
dupfont[k2].append(k)
else:
dupfont[k2] = [k]
del dupfont_cache
return [f_n, font_n_lower, font_family, warning_font]
#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]))
def fnGetFromFamilyName(font_family: str, fn: str, isitalic: int, isbold: int) -> str:
if font_family.get(fn) is not None:
if font_family[fn].get((isitalic, isbold)) is not None:
return font_family[fn][(isitalic, isbold)][0]
elif font_family[fn].get((0, isbold)) is not None:
return font_family[fn][(0, isbold)][0]
elif font_family[fn].get((isitalic, 0)) is not None:
return font_family[fn][(isitalic, 0)][0]
elif font_family[fn].get((0, 0)) is not None:
return font_family[fn][(0, 0)][0]
else:
return fn
else:
return fn
# 系统字体完整性检查,检查是否有ASS所需的全部字体,如果没有,则要求拖入
# 需要以下输入
# fontlist: 字体与其所需字符(只读字体部分) { ASS内的字体名称 : 字符串 }
# font_name: 字体名称与字体路径对应词典 { 字体名称 : [ 字体绝对路径, 字体索引 ] }
# 以下输入可选
# assfont: 结构见下,用于多ASS文件时更新列表
# onlycheck: 缺少字体时不要求输入
# 将会返回以下
# assfont: { 字体绝对路径?字体索引 : [ 字符串, ASS内的字体名称, 修正名称 ]}
# font_name: 同上,用于有新字体拖入时对该词典的更新
def checkAssFont(fontlist: dict, font_info: list, fn_lines: list = [], onlycheck: bool = False):
# 从fontlist获取字体名称
assfont = {}
font_name = font_info[0]
font_n_lower = font_info[1]
font_family = font_info[2]
warning_font = font_info[3]
keys = list(fontlist.keys())
for s in keys:
sp = s.split('?')
isbold = int(sp[2])
isitalic = int(sp[1])
ns = sp[0]
ns = fnGetFromFamilyName(font_family, ns, isitalic, isbold)
cok = False
# 在全局字体名称词典中寻找字体名称
ss = ns
if ns not in font_name:
# 如果找不到,将字体名称统一为小写再次查找
if font_n_lower.get(ns.lower()) is not None:
ss = font_n_lower[ns.lower()]
cok = True
else: cok = True
directout = 0
if not cok:
# 如果 onlycheck 不为 True,向用户要求目标字体
if not onlycheck:
print('\033[1;31m[ERROR] 缺少字体\"{0}\"\n请输入追加的字体文件或其所在字体目录的绝对路径\033[0m'.format(ns))
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 ns.lower() not in addFont[1].keys():
if path.isdir(inFont):
print('\033[1;31m[ERROR] 输入路径中\"{0}\"没有所需字体\"{1}\"\033[0m'.format(inFont, ns))
else: print('\033[1;31m[ERROR] 输入字体\"{0}\"不是所需字体\"{1}\"\033[0m'.format('|'.join(addFont[0].keys()), ns))
inFont = ''
else:
font_name.update(addFont[0])
font_n_lower.update(addFont[1])
font_family.update(addFont[2])
warning_font.extend(addFont[3])
cok = True
else:
print('\033[1;31m[ERROR] 您没有输入任何字符!再回车{0}次回到主菜单\033[0m'.format(3-directout))
directout += 1
inFont = ''
else:
# 否则直接添加空
assfont['?'.join([ns, ns])] = ['', sp[0], ns]
if cok:
if not onlycheck and ss.lower() in warning_font:
print('\033[1;31m[WARNING] 字体\"{0}\"可能不能正常子集化\033[0m'.format(ss))
if warningStop:
print('\033[1;31m[WARNING] 请修复\"{0}\",工作已中断\033[0m'.format(ss))
return None, [font_name, font_n_lower, font_family, warning_font]
if directout < 3:
# 如果找到,添加到assfont列表
font_path = font_name[ss][0]
font_index = font_name[ss][1]
# print(font_name[ss])
dict_key = '?'.join([font_path, str(font_index)])
# 如果 assfont 列表已有该字体,则将新字符添加到 assfont 中
if assfont.get(dict_key) is None:
assfont[dict_key] = [fontlist[s], sp[0], ns]
else:
tfname = assfont[dict_key][2]
newfnamep = assfont[dict_key][1].split('|')
oldstr = fontlist[s]
for newfname in newfnamep:
if sp[0].lower() not in '|'.join(newfnamep).lower():
key1 = 0
key2 = 0
for ii in [0, 1]:
key1 = ii
key2 = 0
if fontlist.get('?'.join([newfname, str(key1), str(0)])) is not None:
break
elif fontlist.get('?'.join([newfname, str(key1), str(1)])) is not None:
key2 = 1
break
newfstr = fontlist['?'.join([newfname, str(key1), str(key2)])]
newfname = '|'.join([sp[0], newfname])
for i in range(0, len(newfstr)):
if newfstr[i] not in oldstr:
oldstr += newfstr[i]
else:
newstr = assfont[dict_key][0]
for i in range(0, len(newstr)):
if newstr[i] not in oldstr:
oldstr += newstr[i]
if ns.lower() not in tfname.lower():
tfname = '|'.join([ns, tfname])
assfont[dict_key] = [oldstr, newfname, tfname]
fontlist[s] = oldstr
# else:
# assfont[dict_key] = [fontlist[s], s]
#print(assfont[dict_key])
if directout >= 3:
return None, [font_name, font_n_lower, font_family, warning_font]
if len(fn_lines) > 0 and not onlycheck:
fn_lines_cache = fn_lines
for i in range(0, len(fn_lines_cache)):
s = fn_lines_cache[i]
fi = s[1][s[1].keys()[0]]
fn_lines[i] = [s[0], {s[1].keys()[0]: [fi[1], fnGetFromFamilyName(font_family, fi[1], fi[2], fi[3])]}]
# print(assfont)
return assfont, [font_name, font_n_lower, font_family, warning_font]
# 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: