├── ASFMKV_GUI-b1.py
├── ASFMKV_GUI-b2.py
├── ASFMKV_pre13.py
├── ASFMKV_py1.00.py
├── ASFMKV_py1.01.py
├── ASFMKV_py1.02-pre.py
├── ASFMKV_py1.02-pre10.py
├── ASFMKV_py1.02-pre11.py
├── ASFMKV_py1.02-pre12E.py
├── ASFMKV_py1.02-pre2.py
├── ASFMKV_py1.02-pre3.py
├── ASFMKV_py1.02-pre4.py
├── ASFMKV_py1.02-pre5.py
├── ASFMKV_py1.02-pre6.py
├── ASFMKV_py1.02-pre7.py
├── ASFMKV_py1.02-pre8.py
├── ASFMKV_py1.02-pre9.py
├── LICENSE
└── README.md
/ASFMKV_py1.00.py:
--------------------------------------------------------------------------------
1 | # -*- coding: UTF-8 -*-
2 | # *************************************************************************
3 | #
4 | # 请使用支持 UTF-8 NoBOM 并最好带有 Python 语法高亮的文本编辑器
5 | # Windows 7 的用户请最好不要使用 写字板/记事本 打开本脚本
6 | #
7 | # *************************************************************************
8 |
9 | # 调用库,请不要修改
10 | import shutil
11 | from fontTools import ttLib
12 | from fontTools import subset
13 | import chardet
14 | from chardet.universaldetector import UniversalDetector
15 | import os
16 | from os import path
17 | import sys
18 | import re
19 | import winreg
20 | import zlib
21 | import json
22 | from colorama import init
23 | from datetime import datetime
24 |
25 | # 初始化环境变量
26 | # *************************************************************************
27 | # 自定义变量
28 | # 修改注意: Python的布尔类型首字母要大写 True 或 False,在名称中有单引号的,需要输入反斜杠转义 \'
29 | # *************************************************************************
30 | # extlist 可输入的视频媒体文件扩展名
31 | extlist = 'mkv;mp4;mts;mpg;flv;mpeg;m2ts;avi;webm;rm;rmvb;mov;mk3d;vob'
32 | # *************************************************************************
33 | # mkvout 媒体文件输出目录(封装)
34 | # 在最前方用"?"标记来表示这是一个子目录
35 | # 注意: 在Python中需要在左侧引号前加 r 来保留 Windows 路径中的反斜杠,路径末尾不需要反斜杠
36 | mkvout = ''
37 | # *************************************************************************
38 | # assout 字幕文件输出目录
39 | # 在最前方用"?"标记来表示这是一个子目录
40 | # 注意: 在Python中需要在左侧引号前加 r 来保留 Windows 路径中的反斜杠,路径末尾不需要反斜杠
41 | assout = '?subs'
42 | # *************************************************************************
43 | # fontout 字体文件输出目录
44 | # 在最前方用"?"标记来表示这是一个子目录
45 | # 注意: 在Python中需要在左侧引号前加 r 来保留 Windows 路径中的反斜杠,路径末尾不需要反斜杠
46 | fontout = '?Fonts'
47 | # *************************************************************************
48 | # fontin 自定义字体文件夹,可做额外字体源,必须是绝对路径
49 | # 可以有多个路径,路径之间用"?"来分隔
50 | # 注意: 在Python中需要在左侧引号前加 r 来保留 Windows 路径中的反斜杠,路径末尾不需要反斜杠
51 | fontin = r''
52 | # *************************************************************************
53 | # notfont (封装)字体嵌入
54 | # True 始终嵌入字体
55 | # False 不嵌入字体,不子集化字体,不替换字幕中的字体信息
56 | notfont = False
57 | # *************************************************************************
58 | # sublang (封装)字幕语言
59 | # 会按照您所输入的顺序给字幕赋予语言编码,如果字幕数多于语言数,多出部分将赋予最后一种语言编码
60 | # IDX+SUB 的 DVDSUB 由于 IDX 文件一般有语言信息,对于DVDSUB不再添加语言编码
61 | # 各语言之间应使用半角分号 ; 隔开,如 'chi;chi;chi;und'
62 | # 可以在 mkvmerge -l 了解语言编码
63 | # 可以只有一个 # 号让程序在运行时再询问您,如 '#'
64 | sublang = ''
65 | # *************************************************************************
66 | # matchStrict 严格匹配
67 | # True 媒体文件名必须在字幕文件名的最前方,如'test.mkv'的字幕可以是'test.ass'或是'test.sub.ass',但不能是'sub.test.ass'
68 | # False 只要字幕文件名中有媒体文件名就行了,不管它在哪
69 | matchStrict = True
70 | # *************************************************************************
71 | # rmAssIn (封装)如果输入文件是mkv,删除mkv文件中原有的字幕
72 | rmAssIn = True
73 | # *************************************************************************
74 | # rmAttach (封装)如果输入文件是mkv,删除mkv文件中原有的附件
75 | rmAttach = True
76 | # *************************************************************************
77 | # v_subdir 视频的子目录搜索
78 | v_subdir = False
79 | # *************************************************************************
80 | # s_subdir 字幕的子目录搜索
81 | s_subdir = False
82 | # *************************************************************************
83 | # copyfont (ListAssFont)拷贝字体到源文件夹
84 | copyfont = False
85 | # *************************************************************************
86 | # resultw (ListAssFont)打印结果到源文件夹
87 | resultw = False
88 | # *************************************************************************
89 |
90 | # 以下变量谨慎更改
91 | # subext 可输入的字幕扩展名,按照python列表语法
92 | subext = ['ass', 'ssa', 'srt', 'sup', 'idx']
93 |
94 |
95 | # 以下环境变量不应更改
96 | # 编译 style行 搜索用正则表达式
97 | style_read = re.compile('.*\nStyle:.*')
98 | cillegal = re.compile(r'[\\/:\*"><\|]')
99 | # 切分extlist列表
100 | extlist = [s.strip(' ').lstrip('.').lower() for s in extlist.split(';') if len(s) > 0]
101 | # 切分sublang列表
102 | sublang = [s.strip(' ').lower() for s in sublang.split(';') if len(s) > 0]
103 | # 切分fontin列表
104 | fontin = [s.strip(' ') for s in fontin.split('?') if len(s) > 0]
105 | fontin = [s for s in fontin if path.isdir(s)]
106 | langlist = []
107 | extsupp = []
108 | dupfont = {}
109 | init()
110 |
111 | # ASS分析部分
112 | # 需要输入
113 | # asspath: ASS文件的绝对路径
114 | # 可选输入
115 | # fontlist: 可以更新fontlist,用于多ASS同时输入的情况,结构见下
116 | # onlycheck: 只确认字幕中的字体,仅仅返回fontlist
117 | # 将会返回
118 | # fullass: 完整的ASS文件内容,以行分割
119 | # fontlist: 字体与其所需字符 { 字体 : 字符串 }
120 | # styleline: 样式内容的起始行
121 | # font_pos: 字体在样式中的位置
122 | def assAnalyze(asspath: str, fontlist: dict = {}, onlycheck: bool = False):
123 | global style_read
124 | # 初始化变量
125 | eventline = 0
126 | style_pos = 0
127 | style_pos2 = 0
128 | text_pos = 0
129 | font_pos = 0
130 | styleline = 0
131 | # 编译分析用正则表达式
132 | event_read = re.compile('.*\nDialogue:.*')
133 | style = re.compile(r'^\[V4.*Styles\]$')
134 | event = re.compile(r'^\[Events\]$')
135 | # 识别文本编码并读取整个SubtitleStationAlpha文件到内存
136 | print('\033[1;33m正在分析字幕: \033[1;37m\"{0}\"\033[0m'.format(path.basename(asspath)))
137 | ass = open(asspath, mode='rb')
138 | if path.getsize(asspath) <= 100 * 1024:
139 | ass_b = ass.read()
140 | ass_code = chardet.detect(ass_b)['encoding'].lower()
141 | else:
142 | detector = UniversalDetector()
143 | for dt in ass:
144 | detector.feed(dt)
145 | if detector.done:
146 | ass_code = detector.result['encoding']
147 | break
148 | detector.reset()
149 | ass.close()
150 | if not ass_code is None:
151 | ass = open(asspath, encoding=ass_code, mode='r')
152 | fullass = ass.readlines()
153 | ass.close()
154 |
155 | # 在文件中搜索Styles标签和Events标签来确认起始行
156 | for s in range(0, len(fullass)):
157 | if re.match(style, fullass[s]) is not None:
158 | styleline = s
159 | elif re.match(event, fullass[s]) is not None:
160 | eventline = s
161 | if styleline != 0 and eventline != 0:
162 | break
163 |
164 | # 获取Style的 Format 行,并用半角逗号分割
165 | style_format = ''.join(fullass[styleline + 1].split(':')[1:]).strip(' ').split(',')
166 | # 确定Style中 Name 和 Fontname 的位置
167 | for i in range(0, len(style_format)):
168 | if style_format[i].lower().strip(' ').replace('\n', '') == 'name':
169 | style_pos = i
170 | elif style_format[i].lower().strip(' ').replace('\n', '') == 'fontname':
171 | font_pos = i
172 | if style_pos != 0 and font_pos != 0:
173 | break
174 |
175 | # 获取 字体表 与 样式字体对应表
176 | style_font = {}
177 | # style_font 词典内容:
178 | # { 样式 : 字体名 }
179 | # fontlist 词典内容:
180 | # { 字体名 : 使用该字体的文本 }
181 | for i in range(styleline + 2, len(fullass)):
182 | if len(fullass[i].split(':')) < 2:
183 | if re.search(style_read, '\n'.join(fullass[i + 1:])) is None:
184 | break
185 | else:
186 | continue
187 | styleStr = ''.join(fullass[i].split(':')[1:]).strip(' ').split(',')
188 | font_key = styleStr[font_pos].lstrip('@')
189 | fontlist.setdefault(font_key, '')
190 | style_font[styleStr[style_pos]] = styleStr[font_pos]
191 |
192 | # 如果 onlycheck 为 True,则不进行下一步的文本提取工作,只返回字体列表
193 | if onlycheck:
194 | fullass = []
195 | style_font.clear()
196 | return None, fontlist, None, None
197 | #print(fontlist)
198 |
199 | # 提取Event的 Format 行,并用半角逗号分割
200 | event_format = ''.join(fullass[eventline + 1].split(':')[1:]).strip(' ').split(',')
201 | # 确定Event中 Style 和 Text 的位置
202 | for i in range(0, len(event_format)):
203 | if event_format[i].lower().replace('\n', '').strip(' ') == 'style':
204 | style_pos2 = i
205 | elif event_format[i].lower().replace('\n', '').strip(' ') == 'text':
206 | text_pos = i
207 | if style_pos2 != 0 and text_pos != 0:
208 | break
209 |
210 | # 获取 字体的字符集
211 | # 先获取 Style,用style_font词典查找对应的 Font
212 | # 再将字符串追加到 fontlist 中对应 Font 的值中
213 | for i in range(eventline + 2, len(fullass)):
214 | eventline_sp = fullass[i].split(':')
215 | if len(eventline_sp) < 2:
216 | if re.search(event_read, '\n'.join(fullass[i + 1:])) is None:
217 | break
218 | else:
219 | continue
220 | #print(fullass[i])
221 | if eventline_sp[0].strip(' ').lower() == 'comment': continue
222 | eventline_sp = ''.join(eventline_sp[1:]).split(',')
223 | eventfont = style_font.get(eventline_sp[style_pos2].lstrip('*'))
224 | if not eventfont is None and not fontlist.get(eventfont) is None:
225 | # 去除行中非文本部分,包括特效标签{},硬软换行符,矢量文本行
226 | eventtext = re.sub(r'(\{.*?\})|(\\[hnN])|(m .*\w*.*)|(\s)', '', ','.join(eventline_sp[text_pos:]))
227 | #print(eventfont, eventtext)
228 | #print(eventtext, ','.join(eventline_sp[text_pos:]))
229 | if len(eventtext) > 0:
230 | for i in range(0,len(eventtext)):
231 | if not eventtext[i] in fontlist[eventfont]:
232 | fontlist[eventfont] = fontlist[eventfont] + eventtext[i]
233 |
234 | print('\033[1m字幕所需字体\033[0m')
235 | fl_popkey = []
236 | # 在字体列表中检查是否有没有在文本中使用的字体,如果有,添加到删去列表
237 | for s in fontlist.keys():
238 | if len(fontlist[s]) == 0:
239 | fl_popkey.append(s)
240 | #print('跳过没有字符的字体\"{0}\"'.format(s))
241 | else: print('\033[1m\"{0}\"\033[0m: 字符数[\033[1;33m{1}\033[0m]'.format(s, len(fontlist[s])))
242 | # 删去 删去列表 中的字体
243 | if len(fl_popkey) > 0:
244 | for s in fl_popkey:
245 | fontlist.pop(s)
246 | return fullass, fontlist, styleline, font_pos
247 |
248 | # 获取字体文件列表
249 | # 接受输入
250 | # customPath: 用户指定的字体文件夹
251 | # font_name: 用于更新font_name(启用从注册表读取名称的功能时有效)
252 | # noreg: 只从用户提供的customPath获取输入
253 | # 将会返回
254 | # filelist: 字体文件清单 [[ 字体绝对路径, 读取位置('': 注册表, '0': 自定义目录) ], ...]
255 | # font_name: 用于更新font_name(启用从注册表读取名称的功能时有效)
256 | def getFileList(customPath: list = [], font_name: dict = {}, noreg: bool = False):
257 | filelist = []
258 |
259 | if not noreg:
260 | # 从注册表读取
261 | fontkey = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, r'SOFTWARE\Microsoft\Windows NT\CurrentVersion\Fonts')
262 | fontkey_num = winreg.QueryInfoKey(fontkey)[1]
263 | #fkey = ''
264 | try:
265 | # 从用户字体注册表读取
266 | fontkey10 = winreg.OpenKey(winreg.HKEY_CURRENT_USER, r'SOFTWARE\Microsoft\Windows NT\CurrentVersion\Fonts')
267 | fontkey10_num = winreg.QueryInfoKey(fontkey10)[1]
268 | if fontkey10_num > 0:
269 | for i in range(fontkey10_num):
270 | p = winreg.EnumValue(fontkey10, i)[1]
271 | #n = winreg.EnumValue(fontkey10, i)[0]
272 | if path.exists(p):
273 | # test = n.split('&')
274 | # if len(test) > 1:
275 | # for i in range(0, len(test)):
276 | # font_name[re.sub(r'\(.*?\)', '', test[i].strip(' '))] = [p, i]
277 | # else: font_name[re.sub(r'\(.*?\)', '', n.strip(' '))] = [p, 0]
278 | filelist.append([p, ''])
279 | # test = path.splitext(path.basename(p))[0].split('&')
280 | except:
281 | pass
282 | for i in range(fontkey_num):
283 | # 从 系统字体注册表 读取
284 | k = winreg.EnumValue(fontkey, i)[1]
285 | #n = winreg.EnumValue(fontkey, i)[0]
286 | pk = path.join(r'C:\Windows\Fonts', k)
287 | if path.exists(pk):
288 | # test = n.split('&')
289 | # if len(test) > 1:
290 | # for i in range(0, len(test)):
291 | # font_name[re.sub(r'\(.*?\)', '', test[i].strip(' '))] = [pk, i]
292 | # else: font_name[re.sub(r'\(.*?\)', '', n.strip(' '))] = [pk, 0]
293 | filelist.append([pk, ''])
294 |
295 | # 从定义的文件夹读取
296 | # fontspath = [r'C:\Windows\Fonts', path.join(os.getenv('USERPROFILE'),r'AppData\Local\Microsoft\Windows\Fonts')]
297 | if customPath is None: customPath == []
298 | if len(customPath) > 0:
299 | print('\033[1;33m请稍等,正在获取自定义文件夹中的字体\033[0m')
300 | for s in customPath:
301 | if not path.isdir(s): continue
302 | for r, d, f in os.walk(s):
303 | for p in f:
304 | p = path.join(r, p)
305 | if path.splitext(p)[1][1:].lower() not in ['ttf', 'ttc', 'otc', 'otf']: continue
306 | filelist.append([path.join(s, p), 'xxx'])
307 |
308 | #print(font_name)
309 | #os.system('pause')
310 | return filelist, font_name
311 |
312 | #字体处理部分
313 | # 需要输入
314 | # fl: 字体文件列表
315 | # 可选输入
316 | # f_n: 默认新建一个,可用于更新font_name
317 | # 将会返回
318 | # font_name: 字体内部名称与绝对路径的索引词典
319 | # 会对以下全局变量进行变更
320 | # dupfont: 重复字体的名称与其路径词典
321 |
322 | # font_name 词典结构
323 | # { 字体名称 : [ 字体绝对路径 , 字体索引 (仅用于TTC/OTC; 如果是TTF/OTF,默认为0) ] }
324 | # dupfont 词典结构
325 | # { 重复字体名称 : [ 字体1绝对路径, 字体2绝对路径, ... ] }
326 | def fontProgress(fl: list, f_n: dict = {}) -> dict:
327 | global dupfont
328 | #print(fl)
329 | flL = len(fl)
330 | print('\033[1;32m正在读取字体信息...\033[0m')
331 | for si in range(0, flL):
332 | s = fl[si][0]
333 | fromCustom = False
334 | if len(fl[si][1]) > 0: fromCustom = True
335 | # 如果有来自自定义文件夹标记,则将 fromCustom 设为 True
336 | ext = path.splitext(s)[1][1:]
337 | # 检查字体扩展名
338 | if ext.lower() not in ['ttf','ttc','otf','otc']: continue
339 | # 输出进度
340 | print('\r' + '\033[1;32m{0}/{1} {2:.2f}% \033[0m'.format(si + 1, flL, ((si + 1)/flL)*100, ), end='', flush=True)
341 | if ext.lower() in ['ttf', 'otf']:
342 | # 如果是 TTF/OTF 单字体文件,则使用 TTFont 读取
343 | try:
344 | tc = [ttLib.TTFont(s, lazy=True)]
345 | except:
346 | print('\033[1;31m\n[ERROR] \"{0}\": {1}\n\033[1;34m[TRY] 正在尝试使用TTC/OTC模式读取\033[0m'.format(s, sys.exc_info()))
347 | # 如果 TTFont 读取失败,可能是使用了错误扩展名的 TTC/OTC 文件,换成 TTCollection 尝试读取
348 | try:
349 | tc = ttLib.TTCollection(s, lazy=True)
350 | print('\033[1;34m[WARNING] 错误的字体扩展名\"{0}\" \033[0m'.format(s))
351 | except:
352 | print('\033[1;31m\n[ERROR] \"{0}\": {1}\033[0m'.format(s, sys.exc_info()))
353 | continue
354 | else:
355 | try:
356 | # 如果是 TTC/OTC 字体集合文件,则使用 TTCollection 读取
357 | tc = ttLib.TTCollection(s, lazy=True)
358 | except:
359 | print('\033[1;31m\n[ERROR] \"{0}\": {1}\n\033[1;34m[TRY] 正在尝试使用TTF/OTF模式读取\033[0m'.format(s, sys.exc_info()))
360 | try:
361 | # 如果读取失败,可能是使用了错误扩展名的 TTF/OTF 文件,用 TTFont 尝试读取
362 | tc = [ttLib.TTFont(s, lazy=True)]
363 | print('\033[1;34m[WARNING] 错误的字体扩展名\"{0}\" \033[0m'.format(s))
364 | except:
365 | print('\033[1;31m\n[ERROR] \"{0}\": {1}\033[0m'.format(s, sys.exc_info()))
366 | continue
367 | #f_n[path.splitext(path.basename(s))[0]] = [s, 0]
368 | for ti in range(0, len(tc)):
369 | t = tc[ti]
370 | # 读取字体的 'name' 表
371 | for ii in range(0, len(t['name'].names)):
372 | name = t['name'].names[ii]
373 | # 若 nameID 为 4,读取 NameRecord 的文本信息
374 | if name.nameID == 4:
375 | namestr = ''
376 | try:
377 | namestr = name.toStr()
378 | except:
379 | # 如果 fontTools 解码失败,则尝试使用 utf-16-be 直接解码
380 | namestr = name.toBytes().decode('utf-16-be', errors='ignore')
381 | try:
382 | # 尝试使用 去除 \x00 字符 解码
383 | if len([i for i in name.toBytes() if i == 0]) > 0:
384 | nnames = t['name']
385 | namebyte = b''.join([bytes.fromhex('{:0>2}'.format(hex(i)[2:])) for i in name.toBytes() if i > 0])
386 | nnames.setName(namebyte,
387 | name.nameID, name.platformID, name.platEncID, name.langID)
388 | namestr = nnames.names[ii].toStr()
389 | print('\n\033[1;33m已修正字体\"{0}\"名称读取 >> \"{1}\"\033[0m'.format(path.basename(s), namestr))
390 | #os.system('pause')
391 | else: namestr = name.toBytes().decode('utf-16-be', errors='ignore')
392 | # 如果没有 \x00 字符,使用 utf-16-be 强行解码;如果有,尝试解码;如果解码失败,使用 utf-16-be 强行解码
393 | except:
394 | print('\n\033[1;33m尝试修正字体\"{0}\"名称读取 >> \"{1}\"\033[0m'.format(path.basename(s), namestr))
395 | if namestr is None: continue
396 | namestr = namestr.strip(' ')
397 | #print(namestr, path.basename(s))
398 | if f_n.get(namestr) is not None:
399 | # 如果发现列表中已有相同名称的字体,检测它的文件名、扩展名、父目录是否相同
400 | # 如果有一者不同且不来自自定义文件夹,添加到重复字体列表
401 | dupp = f_n[namestr][0]
402 | if dupp != s and path.splitext(path.basename(dupp))[0] != path.splitext(path.basename(s))[0] and not fromCustom:
403 | print('\033[1;35m[WARNING] 字体\"{0}\"与字体\"{1}\"的名称\"{2}\"重复!\033[0m'.format(path.basename(f_n[namestr][0]), path.basename(s), namestr))
404 | if dupfont.get(namestr) is not None:
405 | if s not in dupfont[namestr]:
406 | dupfont[namestr].append(s)
407 | else:
408 | dupfont[namestr] = [dupp, s]
409 | else: f_n[namestr] = [s, ti]
410 | #f_n[namestr] = [s, ti]
411 | # if f_n.get(fname) is None: f_n[fname] = [[namestr], s]
412 | # #print(fname, name.toStr(), f_n.get(fname))
413 | # if namestr not in f_n[fname][0]:
414 | # f_n[fname][0] = f_n[fname][0] + [namestr]
415 | tc[0].close()
416 | return f_n
417 |
418 | #print(filelist)
419 | #if path.exists(fontspath10): filelist = filelist.extend(os.listdir(fontspath10))
420 |
421 | #for s in font_name.keys(): print('{0}: {1}'.format(s, font_name[s]))
422 |
423 | # 系统字体完整性检查,检查是否有ASS所需的全部字体,如果没有,则要求拖入
424 | # 需要以下输入
425 | # fontlist: 字体与其所需字符(只读字体部分) { ASS内的字体名称 : 字符串 }
426 | # font_name: 字体名称与字体路径对应词典 { 字体名称 : [ 字体绝对路径, 字体索引 ] }
427 | # 以下输入可选
428 | # assfont: 结构见下,用于多ASS文件时更新列表
429 | # onlycheck: 缺少字体时不要求输入
430 | # 将会返回以下
431 | # assfont: { 字体绝对路径?字体索引 : [ 字符串, ASS内的字体名称 ]}
432 | # font_name: 同上,用于有新字体拖入时对该词典的更新
433 | def checkAssFont(fontlist: dict, font_name: dict, assfont: dict = {}, onlycheck: bool = False):
434 | # 从fontlist获取字体名称
435 | for s in fontlist.keys():
436 | cok = False
437 | # 在全局字体名称词典中寻找字体名称
438 | if s not in font_name:
439 | # 如果找不到,将字体名称统一为小写再次查找
440 | font_name_cache = {}
441 | for ss in font_name.keys():
442 | if ss.lower() == s or ss.upper() == s:
443 | font_name_cache[ss.lower()] = font_name[ss]
444 | cok = True
445 | # update字体名称词典
446 | font_name.update(font_name_cache)
447 | else: cok = True
448 | if not cok:
449 | # 如果 onlycheck 不为 True,向用户要求目标字体
450 | if not onlycheck:
451 | print('\033[1;31m[ERROR] 缺少字体\"{0}\"\n请输入追加的字体文件或其所在字体目录的绝对路径\033[0m'.format(s))
452 | addFont = {}
453 | inFont = ''
454 | while inFont == '':
455 | inFont = input().strip('\"').strip(' ')
456 | if path.exists(inFont):
457 | if path.isdir(inFont):
458 | addFont = fontProgress(getFileList([inFont], noreg=True)[0])
459 | else:
460 | addFont = fontProgress([[inFont, '0']])
461 | if s not in addFont.keys():
462 | if path.isdir(inFont):
463 | print('\033[1;31m[ERROR] 输入路径中\"{0}\"没有所需字体\"{1}\"\033[0m'.format(inFont, s))
464 | else: print('\033[1;31m[ERROR] 输入字体\"{0}\"不是所需字体\"{1}\"\033[0m'.format('|'.join(addFont.keys()), s))
465 | inFont = ''
466 | else:
467 | font_name.update(addFont)
468 | cok = True
469 | else:
470 | print('\033[1;31m[ERROR] 您没有输入任何字符!\033[0m')
471 | inFont = ''
472 | else:
473 | # 否则直接添加空
474 | assfont['?'.join([s, s])] = ['', s]
475 | if cok:
476 | # 如果找到,添加到assfont列表
477 | font_path = font_name[s][0]
478 | font_index = font_name[s][1]
479 | dict_key = '?'.join([font_path, str(font_index)])
480 | # 如果 assfont 列表已有该字体,则将新字符添加到 assfont 中
481 | if assfont.get(dict_key) is None:
482 | assfont[dict_key] = [fontlist[s], s]
483 | else:
484 | if assfont[dict_key][1] == font_index:
485 | newfname = assfont[dict_key][2]
486 | if s != newfname:
487 | newfname = '|'.join([s, newfname])
488 | newstr = assfont[dict_key][1]
489 | newstr2 = ''
490 | for i in range(0, len(newstr)):
491 | if newstr[i] not in fontlist[s]:
492 | newstr2 = newstr2 + newstr[i]
493 | assfont[dict_key] = [fontlist[s] + newstr2, newfname]
494 | else:
495 | assfont[dict_key] = [fontlist[s], s]
496 | #print(assfont[dict_key])
497 |
498 | return assfont, font_name
499 |
500 | # print('正在输出字体子集字符集')
501 | # for s in fontlist.keys():
502 | # logpath = '{0}_{1}.log'.format(path.join(os.getenv('TEMP'), path.splitext(path.basename(asspath))[0]), s)
503 | # log = open(logpath, mode='w', encoding='utf-8')
504 | # log.write(fontlist[s])
505 | # log.close()
506 |
507 | # 字体内部名称变更
508 | def getNameStr(name, subfontcrc: str) -> str:
509 | namestr = ''
510 | nameID = name.nameID
511 | # 变更NameID为1, 3, 4, 6的NameRecord,它们分别对应
512 | # ID Meaning
513 | # 1 Font Family name
514 | # 3 Unique font identifier
515 | # 4 Full font name
516 | # 6 PostScript name for the font
517 | # 注意本脚本并不更改 NameID 为 0 和 7 的版权信息
518 | if nameID in [1,3,4,6]:
519 | namestr = subfontcrc
520 | else:
521 | try:
522 | namestr = name.toStr()
523 | except:
524 | namestr = name.toBytes().decode('utf-16-be', errors='ignore')
525 | return namestr
526 |
527 | # 字体子集化
528 | # 需要以下输入:
529 | # assfont: { 字体绝对路径?字体索引 : [ 字符串, ASS内的字体名称 ]}
530 | # fontdir: 新字体存放目录
531 | # 将会返回以下:
532 | # newfont_name: { 原字体名 : [ 新字体绝对路径, 新字体名 ] }
533 | def assFontSubset(assfont: dict, fontdir: str) -> dict:
534 | newfont_name = {}
535 | # print(fontdir)
536 | print('正在子集化……')
537 |
538 | if path.exists(path.dirname(fontdir)):
539 | if not path.isdir(fontdir):
540 | try:
541 | os.mkdir(fontdir)
542 | except:
543 | print('\033[1;31m[ERROR] 创建文件夹\"{0}\"失败\033[0m'.format(fontdir))
544 | fontdir = os.getcwd()
545 | if not path.isdir(fontdir): fontdir = path.dirname(fontdir)
546 | else: fontdir = os.getcwd()
547 | print('\033[1;33m字体输出路径: \"{0}\"\033[0m'.format(fontdir))
548 |
549 | for k in assfont.keys():
550 | # 偷懒没有变更该函数中的assfont解析到新的词典格式
551 | # 在这里会将assfont词典转换为旧的assfont列表形式
552 | # assfont: [ 字体绝对路径, 字体索引, 字符串, ASS内的字体名称 ]
553 | s = k.split('?') + [assfont[k][0], assfont[k][1]]
554 | subfontext = ''
555 | fontext = path.splitext(path.basename(s[0]))[1]
556 | if fontext[1:].lower() in ['otc', 'ttc']:
557 | subfontext = fontext[:3] + 'f'
558 | else: subfontext = fontext
559 | #print(fontdir, path.exists(path.dirname(fontdir)), path.exists(fontdir))
560 | fontname = re.sub(cillegal, '_', s[3])
561 | subfontpath = path.join(fontdir, fontname + subfontext)
562 | #print(fontdir, subfontpath)
563 | # if not path.exists(path.dirname(subfontpath)):
564 | # try:
565 | # os.mkdir(path.dirname(subfontpath))
566 | # except:
567 | # subfontpath = path.join(fontdir, fontname + subfontext)
568 | # print('\033[1;31m[ERROR] 创建文件夹\"{0}\"失败\033[0m'.format(fontdir))
569 | subsetarg = [s[0], '--text={0}'.format(s[2]), '--output-file={0}'.format(subfontpath), '--font-number={0}'.format(s[1]), '--passthrough-tables']
570 | try:
571 | subset.main(subsetarg)
572 | except PermissionError:
573 | print('\033[1;31m[ERROR] 文件\"{0}\"访问失败\033[0m'.format(path.basename(subfontpath)))
574 | except:
575 | # print('\033[1;31m[ERROR] 失败字符串: \"{0}\" \033[0m'.format(s[2]))
576 | print('\033[1;31m[ERROR] {0}\033[0m'.format(sys.exc_info()))
577 | print('\033[1;31m[WARNING] 字体\"{0}\"子集化失败,将会保留完整字体\033[0m'.format(path.basename(s[0])))
578 | # crcnewf = ''.join([path.splitext(subfontpath)[0], fontext])
579 | # shutil.copy(s[0], crcnewf)
580 | ttLib.TTFont(s[0], lazy=False, fontNumber=int(s[1])).save(subfontpath, False)
581 | subfontcrc = None
582 | # newfont_name[s[3]] = [crcnewf, subfontcrc]
583 | newfont_name[s[3]] = [subfontpath, subfontcrc]
584 | continue
585 | #os.system('pyftsubset {0}'.format(' '.join(subsetarg)))
586 | if path.exists(subfontpath):
587 | subfontbyte = open(subfontpath, mode='rb')
588 | subfontcrc = str(hex(zlib.crc32(subfontbyte.read())))[2:].upper()
589 | if len(subfontcrc) < 8: subfontcrc = '0' + subfontcrc
590 | # print('CRC32: {0} \"{1}\"'.format(subfontcrc, path.basename(s[0])))
591 | subfontbyte.close()
592 | rawf = ttLib.TTFont(s[0], lazy=True, fontNumber=int(s[1]))
593 | newf = ttLib.TTFont(subfontpath, lazy=False)
594 | if len(newf['name'].names) == 0:
595 | for i in range(0,7):
596 | if len(rawf['name'].names) - 1 >= i:
597 | name = rawf['name'].names[i]
598 | namestr = getNameStr(name, subfontcrc)
599 | newf['name'].addName(namestr, minNameID=-1)
600 | else:
601 | for i in range(0, len(rawf['name'].names)):
602 | name = rawf['name'].names[i]
603 | nameID = name.nameID
604 | platID = name.platformID
605 | langID = name.langID
606 | platEncID = name.platEncID
607 | namestr = getNameStr(name, subfontcrc)
608 | newf['name'].setName(namestr ,nameID, platID, platEncID, langID)
609 | if len(newf.getGlyphOrder()) == 1 and '.notdef' in newf.getGlyphOrder():
610 | print('\033[1;31m[WARNING] 字体\"{0}\"子集化失败,将会保留完整字体\033[0m'.format(path.basename(s[0])))
611 | crcnewf = subfontpath
612 | newf.close()
613 | if not subfontpath == s[0]: os.remove(subfontpath)
614 | # shutil.copy(s[0], crcnewf)
615 | rawf.save(crcnewf, False)
616 | subfontcrc = None
617 | else:
618 | crcnewf = '.{0}'.format(subfontcrc).join(path.splitext(subfontpath))
619 | newf.save(crcnewf)
620 | newf.close()
621 | rawf.close()
622 | if path.exists(crcnewf):
623 | if not subfontpath == crcnewf: os.remove(subfontpath)
624 | newfont_name[s[3]] = [crcnewf, subfontcrc]
625 | #print(newfont_name)
626 | return newfont_name
627 |
628 | # 更改ASS样式对应的字体
629 | # 需要以下输入
630 | # fullass: 完整的ass文件内容,以行分割为列表
631 | # newfont_name: { 原字体名 : [ 新字体路径, 新字体名 ] }
632 | # asspath: 原ass文件的绝对路径
633 | # styleline: [V4/V4+ Styles]标签在SSA/ASS中的行数,对应到fullass列表的索引数
634 | # font_pos: Font参数在 Styles 的 Format 中位于第几个逗号之后
635 | # 以下输入可选
636 | # outdir: 新字幕的输出目录,默认为源文件目录
637 | # ncover: 为True时不覆盖原有文件,为False时覆盖
638 | # 将会返回以下
639 | # newasspath: 新ass文件的绝对路径
640 | def assFontChange(fullass: list, newfont_name: dict, asspath: str, styleline: int,
641 | font_pos: int, outdir: str = '', ncover: bool = False) -> str:
642 | # 扫描Style各行,并替换掉字体名称
643 | for i in range(styleline + 2, len(fullass)):
644 | if len(fullass[i].split(':')) < 2:
645 | if re.search(style_read, '\n'.join(fullass[i + 1:])) is None:
646 | break
647 | else:
648 | continue
649 | styleStr = ''.join(fullass[i].split(':')[1:]).strip(' ').split(',')
650 | fontstr = styleStr[font_pos].lstrip('@')
651 | if not newfont_name.get(fontstr) is None:
652 | if not newfont_name[fontstr][1] is None:
653 | fullass[i] = fullass[i].replace(fontstr, newfont_name[fontstr][1])
654 | if path.exists(path.dirname(outdir)):
655 | if not path.isdir(outdir):
656 | try:
657 | os.mkdir(outdir)
658 | except:
659 | print('\033[1;31m[ERROR] 创建文件夹\"{0}\"失败\033[0m'.format(outdir))
660 | outdir = os.getcwd()
661 | print('\033[1;33m字幕输出路径: \"{0}\"\033[0m'.format(outdir))
662 | if path.isdir(outdir):
663 | newasspath = path.join(outdir, '.subset'.join(path.splitext(path.basename(asspath))))
664 | else: newasspath = '.subset'.join(path.splitext(asspath))
665 | if path.exists(newasspath) and ncover:
666 | testpathl = path.splitext(newasspath)
667 | testc = 1
668 | testpath = '{0}#{1}{2}'.format(testpathl[0], testc, testpathl[1])
669 | while path.exists(testpath):
670 | testc += 1
671 | testpath = '{0}#{1}{2}'.format(testpathl[0], testc, testpathl[1])
672 | newasspath = testpath
673 | ass = open(newasspath, mode='w', encoding='utf-8')
674 | ass.writelines(fullass)
675 | ass.close()
676 | #print('ASS样式转换完成: {0}'.format(path.basename(newasspath)))
677 | return newasspath
678 |
679 | # ASFMKV,将媒体文件、字幕、字体封装到一个MKV文件,需要mkvmerge命令行支持
680 | # 需要以下输入
681 | # file: 媒体文件绝对路径
682 | # outfile: 输出文件的绝对路径,如果该选项空缺,默认为 输入媒体文件.muxed.mkv
683 | # asslangs: 赋值给字幕轨道的语言,如果字幕轨道多于asslangs的项目数,超出部分将全部应用asslangs的末项
684 | # asspaths: 字幕绝对路径列表
685 | # fontpaths: 字体列表,格式为 [[字体1绝对路径], [字体1绝对路径], ...],必须嵌套一层,因为主函数偷懒了
686 | # 将会返回以下
687 | # mkvmr: mkvmerge命令行的返回值
688 | def ASFMKV(file: str, outfile: str = '', asslangs: list = [], asspaths: list = [], fontpaths: list = []) -> int:
689 | #print(fontpaths)
690 | global rmAssIn, rmAttach, mkvout, notfont
691 | if file is None: return 4
692 | elif file == '': return 4
693 | elif not path.exists(file) or not path.isfile(file): return 4
694 | if outfile is None: outfile = ''
695 | if outfile == '' or not path.exists(path.dirname(outfile)):
696 | outfile = '.muxed'.join([path.splitext(file)[0], '.mkv'])
697 | outfile = path.splitext(outfile)[0] + '.mkv'
698 | if path.exists(outfile):
699 | checkloop = 1
700 | while path.exists('#{0}'.format(checkloop).join(path.splitext(outfile))):
701 | checkloop += 1
702 | outfile = '#{0}'.format(checkloop).join([path.splitext(outfile)[0], '.mkv'])
703 | mkvargs = []
704 | if rmAssIn: mkvargs.append('-S')
705 | if rmAttach: mkvargs.append('-M')
706 | mkvargs.extend(['(', file, ')'])
707 | fn = path.splitext(path.basename(file))[0]
708 | if len(asspaths) > 0:
709 | for i in range(0, len(asspaths)):
710 | s = asspaths[i]
711 | assfn = path.splitext(path.basename(s))[0]
712 | assnote = assfn[(assfn.find(fn) + len(fn)):].replace('.subset', '')
713 | #print(assfn, fn, assnote)
714 | if len(assnote) > 1:
715 | mkvargs.extend(['--track-name', '0:{0}'.format(assnote.lstrip('.'))])
716 | if len(asslangs) > 0 and path.splitext(s)[1][1:].lower() not in ['idx']:
717 | mkvargs.append('--language')
718 | if i < len(asslangs):
719 | mkvargs.append('0:{0}'.format(asslangs[i]))
720 | else:
721 | mkvargs.append('0:{0}'.format(asslangs[len(asslangs) - 1]))
722 | mkvargs.extend(['(', s, ')'])
723 | if len(fontpaths) > 0:
724 | for s in fontpaths:
725 | fext = path.splitext(s[0])[1][1:].lower()
726 | if fext in ['ttf', 'ttc']:
727 | mkvargs.extend(['--attachment-mime-type', 'application/x-truetype-font'])
728 | elif fext in ['otf', 'otc']:
729 | mkvargs.extend(['--attachment-mime-type', 'application/vnd.ms-opentype'])
730 | mkvargs.extend(['--attach-file', s[0]])
731 | mkvargs.extend(['--title', fn])
732 | mkvjsonp = path.splitext(file)[0] + '.mkvmerge.json'
733 | mkvjson = open(mkvjsonp, mode='w', encoding='utf-8')
734 | json.dump(mkvargs, fp=mkvjson, sort_keys=True, indent=2, separators=(',', ': '))
735 | mkvjson.close()
736 | mkvmr = os.system('mkvmerge @\"{0}\" -o \"{1}\"'.format(mkvjsonp, outfile))
737 | if mkvmr > 1:
738 | print('\n\033[1;31m[ERROR] 检测到不正常的mkvmerge返回值,重定向输出...\033[0m')
739 | os.system('mkvmerge -r \"{0}\" @\"{1}\" -o NUL'.format('{0}.{1}.log'
740 | .format(path.splitext(file)[0], datetime.now().strftime('%Y-%m%d-%H%M-%S_%f')), mkvjsonp))
741 | elif not notfont:
742 | for p in asspaths:
743 | print('\033[1;32m封装成功: \033[1;37m\"{0}\"\033[0m'.format(p))
744 | if path.splitext(p)[1][1:].lower() in ['ass', 'ssa']:
745 | try:
746 | os.remove(p)
747 | except:
748 | print('\033[1;33m[ERROR] 文件\"{0}\"删除失败\033[0m'.format(p))
749 | for f in fontpaths:
750 | print('\033[1;32m封装成功: \033[1;37m\"{0}\"\033[0m'.format(f[0]))
751 | try:
752 | os.remove(f[0])
753 | except:
754 | print('\033[1;33m[ERROR] 文件\"{0}\"删除失败\033[0m'.format(f[0]))
755 | try:
756 | os.remove(mkvjsonp)
757 | except:
758 | print('\033[1;33m[ERROR] 文件\"{0}\"删除失败\033[0m'.format(mkvjsonp))
759 | print('\033[1;32m输出成功: \"{0}\"\033[0m'.format(outfile))
760 | else:
761 | print('\033[1;32m输出成功: \"{0}\"\033[0m'.format(outfile))
762 | return mkvmr
763 |
764 | # 从输入的目录中获取媒体文件列表
765 | # 需要以下输入
766 | # dir: 要搜索的目录
767 | # 返回以下结果
768 | # medias: 多媒体文件列表
769 | # 结构: [[ 文件名(无扩展名), 绝对路径 ], ...]
770 | def getMediaFilelist(dir: str) -> list:
771 | medias = []
772 | global v_subdir, extlist
773 | if path.isdir(dir):
774 | if v_subdir:
775 | for r,ds,fs in os.walk(dir):
776 | for f in fs:
777 | if path.splitext(f)[1][1:].lower() in extlist:
778 | medias.append([path.splitext(path.basename(f))[0], path.join(r, f)])
779 | else:
780 | for f in os.listdir(dir):
781 | if path.isfile(path.join(dir, f)):
782 | if path.splitext(f)[1][1:].lower() in extlist:
783 | medias.append([path.splitext(path.basename(f))[0], path.join(dir, f)])
784 | return medias
785 |
786 | # 在目录中找到与媒体文件列表中的媒体文件对应的字幕
787 | # 遵循以下原则
788 | # 媒体文件在上级目录,则匹配子目录中的字幕;媒体文件的字幕只能在媒体文件的同一目录或子目录中,不能在上级目录和其他同级目录
789 | # 需要以下输入
790 | # medias: 媒体文件列表,结构见 getMediaFilelist
791 | # cpath: 开始搜索的顶级目录
792 | # 将会返回以下
793 | # media_ass: 媒体文件与字幕文件的对应词典
794 | # 结构: { 媒体文件绝对路径 : [ 字幕1绝对路径, 字幕2绝对路径, ...] }
795 | def getSubtitles(medias: list, cpath: str) -> dict:
796 | media_ass = {}
797 | global s_subdir, matchStrict
798 | if s_subdir:
799 | for r,ds,fs in os.walk(cpath):
800 | for f in [path.join(r, s) for s in fs if path.splitext(s)[1][1:].lower() in subext]:
801 | if '.subset' in path.basename(f): continue
802 | for l in medias:
803 | vdir = path.dirname(l[1])
804 | sdir = path.dirname(f)
805 | sext = path.splitext(f)
806 | if (l[0] in f and not matchStrict) or (l[0] == path.basename(f)[:len(l[0])] and matchStrict):
807 | if((vdir in sdir and sdir not in vdir) or (vdir == sdir)):
808 | if sext[1][:1].lower() == 'idx':
809 | if not path.exists(sext[1] + '.sub'):
810 | continue
811 | if media_ass.get(l[1]) is None:
812 | media_ass[l[1]] = [f]
813 | else: media_ass[l[1]].append(f)
814 | else:
815 | for f in [path.join(cpath, s) for s in os.listdir(cpath) if not path.isdir(s) and
816 | path.splitext(s)[1][1:].lower() in subext]:
817 | # print(f, cpath)
818 | if '.subset' in path.basename(f): continue
819 | for l in medias:
820 | # print(path.basename(f)[len(l[0]):], l)
821 | sext = path.splitext(f)
822 | if (l[0] in f and not matchStrict) or (l[0] == path.basename(f)[:len(l[0])] and matchStrict):
823 | if path.dirname(l[1]) == path.dirname(f):
824 | if sext[1][:1].lower() == 'idx':
825 | if not path.exists(sext[1] + '.sub'):
826 | continue
827 | if media_ass.get(l[1]) is None:
828 | media_ass[l[1]] = [f]
829 | else: media_ass[l[1]].append(f)
830 | return media_ass
831 |
832 | # 主函数,负责调用各函数走完完整的处理流程
833 | # 需要以下输入
834 | # font_name: 字体名称与字体路径对应词典,结构见 fontProgress
835 | # asspath: 字幕绝对路径列表
836 | # 以下输入可选
837 | # outdir: 输出目录,格式 [ 字幕输出目录, 字体输出目录, 视频输出目录 ],如果项数不足,则取最后一项;默认为 asspaths 中每项所在目录
838 | # mux: 不要封装,只运行到子集化完成
839 | # vpath: 视频路径,只在 mux = True 时生效
840 | # asslangs: 字幕语言列表,将会按照顺序赋给对应的字幕轨道,只在 mux = True 时生效
841 | # 将会返回以下
842 | # newasspath: 列表,新生成的字幕文件绝对路径
843 | # newfont_name: 词典,{ 原字体名 : [ 新字体绝对路径, 新字体名 ] }
844 | # ??? : 数值,mkvmerge的返回值;如果 mux = False,返回-1
845 | def main(font_name: dict, asspath: list, outdir: list = ['', '', ''], mux: bool = False, vpath: str = '', asslangs: list = []):
846 | print('')
847 | outdir_temp = outdir[:3]
848 | outdir = ['', '', '']
849 | for i in range(0, len(outdir_temp)):
850 | s = outdir_temp[i]
851 | # print(s)
852 | if s is None:
853 | outdir[i] = ''
854 | elif s == '':
855 | outdir[i] = s
856 | else:
857 | try:
858 | if not path.isdir(s) and path.exists(path.dirname(s)):
859 | os.mkdir(s)
860 | if path.isdir(s): outdir[i] = s
861 | except:
862 | print('\033[1;31m[ERROR] 创建输出文件夹错误\n[ERROR] {0}\033[0m'.format(sys.exc_info()))
863 | if '\\' in s:
864 | outdir[i] = path.join(os.getcwd(), path.basename(s.rstrip('\\')))
865 | else: outdir[i] = path.join(os.getcwd(), s)
866 | # print(outdir)
867 | # os.system('pause')
868 | global notfont
869 | # multiAss 多ASS文件输入记录词典
870 | # 结构: { ASS文件绝对路径 : [ 完整ASS文件内容(fullass), 样式位置(styleline), 字体在样式行中的位置(font_pos) ]}
871 | multiAss = {}
872 | assfont = {}
873 | fontlist = {}
874 | newasspath = []
875 | fo = ''
876 | if not notfont:
877 | # print('\n字体名称总数: {0}'.format(len(font_name.keys())))
878 | # noass = False
879 | for i in range(0, len(asspath)):
880 | s = asspath[i]
881 | if path.splitext(s)[1][1:].lower() not in ['ass', 'ssa']:
882 | multiAss[s] = [[], 0, 0]
883 | else:
884 | print('正在分析字幕文件: \"{0}\"'.format(path.basename(s)))
885 | fullass, fontlist, styleline, font_pos = assAnalyze(s, fontlist)
886 | multiAss[s] = [fullass, styleline, font_pos]
887 | assfont, font_name = checkAssFont(fontlist, font_name, assfont)
888 | sn = path.splitext(path.basename(asspath[0]))[0]
889 | fn = path.join(path.dirname(asspath[0]), 'Fonts')
890 | if outdir[1] == '': outdir[1] = fn
891 | if not path.isdir(outdir[1]):
892 | try:
893 | os.mkdir(outdir[1])
894 | fo = path.join(outdir[1], sn)
895 | except:
896 | fo = path.join(path.dirname(outdir[1]), sn)
897 | else:
898 | fo = path.join(outdir[1], sn)
899 | newfont_name = assFontSubset(assfont, fo)
900 | for s in asspath:
901 | if path.splitext(s)[1][1:].lower() not in ['ass', 'ssa']:
902 | newasspath.append(s)
903 | elif len(multiAss[s][0]) == 0 or multiAss[s][1] == multiAss[s][2]:
904 | continue
905 | else: newasspath.append(assFontChange(multiAss[s][0], newfont_name, s, multiAss[s][1], multiAss[s][2], outdir[0]))
906 | else:
907 | newasspath = asspath
908 | newfont_name = {}
909 | if mux:
910 | if outdir[2] == '': outdir[2] = path.dirname(vpath)
911 | if not path.isdir(outdir[2]):
912 | try:
913 | os.mkdir(outdir[2])
914 | except:
915 | outdir[2] = path.dirname(outdir[2])
916 | mkvr = ASFMKV(vpath, path.join(outdir[2], path.splitext(path.basename(vpath))[0] + '.mkv'),
917 | asslangs=asslangs, asspaths=newasspath, fontpaths=list(newfont_name.values()))
918 | if not notfont:
919 | for ap in newasspath:
920 | if path.exists(path.dirname(ap)) and path.splitext(ap)[1][1:].lower() not in ['ass', 'ssa']:
921 | try:
922 | os.rmdir(path.dirname(ap))
923 | except:
924 | break
925 | for fp in newfont_name.keys():
926 | if path.exists(path.dirname(newfont_name[fp][0])):
927 | try:
928 | os.rmdir(path.dirname(newfont_name[fp][0]))
929 | except:
930 | continue
931 | if path.isdir(fo):
932 | try:
933 | os.rmdir(fo)
934 | except:
935 | pass
936 | return newasspath, newfont_name, mkvr
937 | else:
938 | return newasspath, newfont_name, -1
939 |
940 | def cls():
941 | os.system('cls')
942 |
943 |
944 | # 初始化字体列表 和 mkvmerge 相关参数
945 | os.system('title ASFMKV Python Remake 1.00 ^| (c) 2022 yyfll ^| Apache-2.0')
946 | fontlist, font_name = getFileList(fontin)
947 | font_name = fontProgress(fontlist, font_name)
948 | no_mkvm = False
949 | no_cmdc = False
950 | mkvmv = '\n\033[1;33m没有检测到 mkvmerge\033[0m'
951 | if os.system('mkvmerge -V 1>nul 2>nul') > 0:
952 | no_mkvm = True
953 | else:
954 | print('\n\n\033[1;33m正在获取 mkvmerge 语言编码列表和支持格式列表,请稍等...\033[0m')
955 | mkvmv = '\n' + os.popen('mkvmerge -V --ui-language en', mode='r').read().replace('\n', '')
956 | extget = re.compile(r'\[.*\]')
957 | langmkv = os.popen('mkvmerge --list-languages', mode='r')
958 | for s in langmkv.buffer.read().decode('utf-8').splitlines()[2:]:
959 | s = s.replace('\n', '').split('|')
960 | for si in range(1, len(s)):
961 | ss = s[si]
962 | if len(ss.strip(' ')) > 0:
963 | langlist.append(ss.strip(' '))
964 | langmkv.close()
965 | for s in os.popen('mkvmerge -l --ui-language en', mode='r').readlines()[1:]:
966 | for ss in re.search(r'\[.*\]', s).group().lstrip('[').rstrip(']').split(' '):
967 | extsupp.append(ss)
968 | extl_c = extlist
969 | extlist = []
970 | print('')
971 | for i in range(0, len(extl_c)):
972 | s = extl_c[i]
973 | if s in extsupp:
974 | extlist.append(s)
975 | else:
976 | print('\033[1;31m[WARNING] 您设定的媒体扩展名 {0} 无效,已从列表移除\033[0m'.format(s))
977 | if len(extlist) != len(extl_c):
978 | print('\n\033[1;33m当前的媒体扩展名列表: \"{0}\"\033[0m\n'.format(';'.join(extlist)))
979 | os.system('pause')
980 | del extl_c
981 | if len(sublang) > 0:
982 | print('')
983 | sublang_c = sublang
984 | sublang = []
985 | for i in range(0, len(sublang_c)):
986 | s = sublang_c[i]
987 | if s in langlist:
988 | sublang.append(s)
989 | else:
990 | print('\033[1;31m[WARNING] 您设定的语言编码 {0} 无效,已从列表移除\033[0m'.format(s))
991 | if len(sublang) != len(sublang_c):
992 | print('\n\033[1;33m当前的语言编码列表: \"{0}\"\033[0m\n'.format(';'.join(sublang)))
993 | os.system('pause')
994 | del sublang_c
995 |
996 |
997 | def cListAssFont(font_name):
998 | global resultw, s_subdir, copyfont
999 | leave = True
1000 | while leave:
1001 | cls()
1002 | print('''ASFMKV-ListAssFontPy
1003 | 注意: 本程序由于设计原因,列出的是字体文件与其内部字体名的对照表
1004 |
1005 | 选择功能:
1006 | [L] 回到上级菜单
1007 | [A] 列出全部字体
1008 | [B] 检查并列出字幕所需字体
1009 |
1010 | 切换开关:
1011 | [1] 将结果写入文件: \033[1;33m{0}\033[0m
1012 | [2] 拷贝所需字体: \033[1;33m{1}\033[0m
1013 | [3] 搜索子目录(字幕): \033[1;33m{2}\033[0m
1014 | '''.format(resultw, copyfont, s_subdir))
1015 | work = os.system('choice /M 请输入 /C AB123L')
1016 | if work == 1:
1017 | cls()
1018 | wfilep = path.join(os.getcwd(), datetime.now().strftime('ASFMKV_FullFont_%Y-%m%d-%H%M-%S_%f.log'))
1019 | if resultw:
1020 | wfile = open(wfilep, mode='w', encoding='utf-8-sig')
1021 | else: wfile = None
1022 | fn = ''
1023 | print('FontList', file=wfile)
1024 | for s in font_name.keys():
1025 | nfn = path.basename(font_name[s][0])
1026 | if fn != nfn:
1027 | if wfile is not None:
1028 | print('>\n{0} <{1}'.format(nfn, s), end='', file=wfile)
1029 | else: print('>\033[0m\n\033[1;36m{0}\033[0m \033[1m<{1}'.format(nfn, s), end='')
1030 | else:
1031 | print(', {0}'.format(s), end='', file=wfile)
1032 | fn = nfn
1033 | if wfile is not None:
1034 | print(wfilep)
1035 | print('>', file=wfile)
1036 | wfile.close()
1037 | else: print('>\033[0m')
1038 | elif work == 2:
1039 | cls()
1040 | cpath = ''
1041 | directout = False
1042 | while not path.exists(cpath) and not directout:
1043 | directout = True
1044 | cpath = input('请输入目录路径或字幕文件路径: ').strip('\"')
1045 | if cpath == '' : print('没有输入,回到上级菜单')
1046 | elif not path.exists(cpath): print('\033[1;31m[ERROR] 找不到路径: \"{0}\"\033[0m'.format(cpath))
1047 | elif not path.isfile(cpath) and not path.isdir(cpath): print('\033[1;31m[ERROR] 输入的必须是文件或目录!: \"{0}\"\033[0m'.format(cpath))
1048 | elif not path.isabs(cpath): print('\033[1;31m[ERROR] 路径必须为绝对路径!: \"{0}\"\033[0m'.format(cpath))
1049 | elif path.isdir(cpath): directout = False
1050 | elif not path.splitext(cpath)[1][1:].lower() in ['ass', 'ssa']:
1051 | print('\033[1;31m[ERROR] 输入的必须是ASS/SSA字幕文件!: \"{0}\"\033[0m'.format(cpath))
1052 | else: directout = False
1053 | #clist = []
1054 | fontlist = {}
1055 | assfont = {}
1056 | if not directout:
1057 | if path.isdir(cpath):
1058 | #print(cpath)
1059 | dir = ''
1060 | if s_subdir:
1061 | for r,ds,fs in os.walk(cpath):
1062 | for f in fs:
1063 | if path.splitext(f)[1][1:].lower() in ['ass', 'ssa']:
1064 | a, fontlist, b, c = assAnalyze(path.join(r, f), fontlist, onlycheck=True)
1065 | assfont, font_name = checkAssFont(fontlist, font_name, assfont, onlycheck=True)
1066 | else:
1067 | for f in os.listdir(cpath):
1068 | if path.isfile(path.join(cpath, f)):
1069 | #print(f, path.splitext(f)[1][1:].lower())
1070 | if path.splitext(f)[1][1:].lower() in ['ass', 'ssa']:
1071 | #print(f, 'pass')
1072 | a, fontlist, b, c = assAnalyze(path.join(cpath, f), fontlist, onlycheck=True)
1073 | assfont, font_name = checkAssFont(fontlist, font_name, assfont, onlycheck=True)
1074 | fd = path.join(cpath, 'Fonts')
1075 | else:
1076 | a, fontlist, b, c = assAnalyze(cpath, fontlist, onlycheck=True)
1077 | assfont, font_name = checkAssFont(fontlist, font_name, assfont, onlycheck=True)
1078 | fd = path.join(path.dirname(cpath), 'Fonts')
1079 | if len(assfont.keys()) < 1:
1080 | print('\033[1;31m[ERROR] 目标路径没有ASS/SSA字幕文件\033[0m')
1081 | else:
1082 | wfile = None
1083 | print('')
1084 | if copyfont or resultw:
1085 | if not path.isdir(fd): os.mkdir(fd)
1086 | if resultw:
1087 | wfile = open(path.join(cpath, 'Fonts', 'fonts.txt'), mode='w', encoding='utf-8-sig')
1088 | for s in assfont.keys():
1089 | ssp = s.split('?')
1090 | if not ssp[0] == ssp[1]:
1091 | fp = ssp[0]
1092 | fn = path.basename(fp)
1093 | ann = ''
1094 | errshow = False
1095 | if copyfont:
1096 | try:
1097 | shutil.copy(fp, path.join(fd, fn))
1098 | ann = ' - copied'
1099 | except:
1100 | print('[ERROR]', sys.exc_info())
1101 | ann = ' - copy error'
1102 | errshow = True
1103 | if resultw:
1104 | print('{0} <{1}>{2}'.format(assfont[s][1], path.basename(fn), ann), file=wfile)
1105 | if errshow:
1106 | print('\033[1;36m{0}\033[0m \033[1m<{1}>\033[1;31m{2}\033[0m'.format(assfont[s][1], path.basename(fn), ann))
1107 | else: print('\033[1;36m{0}\033[0m \033[1m<{1}>\033[1;32m{2}\033[0m'.format(assfont[s][1], path.basename(fn), ann))
1108 | else:
1109 | if resultw:
1110 | print('{0} - No Found'.format(ssp[0]), file=wfile)
1111 | print('\033[1;36m{0}\033[1;31m - No Found\033[0m'.format(ssp[0]))
1112 | if resultw: wfile.close()
1113 | print('')
1114 | elif work == 3:
1115 | if resultw: resultw = False
1116 | else: resultw = True
1117 | elif work == 4:
1118 | if copyfont: copyfont = False
1119 | else: copyfont = True
1120 | elif work == 5:
1121 | if s_subdir: s_subdir = False
1122 | else: s_subdir = True
1123 | else:
1124 | leave = False
1125 | if work < 3: os.system('pause')
1126 |
1127 |
1128 | def checkOutPath(op: str, default: str) -> str:
1129 | if op == '': return default
1130 | if op[0] == '?': return '?' + re.sub(cillegal, '_', op[1:])
1131 | if not path.isabs(op):
1132 | print('\033[1;31m[ERROR] 输入的必须是绝对路径或子目录名称!: \"{0}\"\033[0m'.format(op))
1133 | os.system('pause')
1134 | return default
1135 | if path.isdir(op): return op
1136 | if path.isfile(op): return path.dirname(op)
1137 | print('\033[1;31m[ERROR] 输入的必须是目录路径或子目录名称!: \"{0}\"\033[0m'.format(op))
1138 | os.system('pause')
1139 | return default
1140 |
1141 | def cFontSubset(font_name):
1142 | global extlist, v_subdir, s_subdir, rmAssIn, rmAttach, \
1143 | mkvout, assout, fontout, matchStrict, no_mkvm, notfont
1144 | leave = True
1145 | while leave:
1146 | cls()
1147 | print('''ASFMKV & ASFMKV-FontSubset
1148 |
1149 | 选择功能:
1150 | [L] 回到上级菜单
1151 | [A] 子集化字体
1152 | [B] 子集化并封装
1153 |
1154 | 切换开关:
1155 | [1] 检视媒体扩展名列表 及 语言编码列表
1156 | [2] 搜索子目录(视频): \033[1;33m{0}\033[0m
1157 | [3] 搜索子目录(字幕): \033[1;33m{1}\033[0m
1158 | [4] (封装)移除内挂字幕: \033[1;33m{2}\033[0m
1159 | [5] (封装)移除原有附件: \033[1;33m{3}\033[0m
1160 | [6] (封装)不封装字体: \033[1;33m{8}\033[0m
1161 | [7] 严格字幕匹配: \033[1;33m{7}\033[0m
1162 | [8] 媒体文件输出文件夹: \033[1;33m{4}\033[0m
1163 | [9] 字幕文件输出文件夹: \033[1;33m{5}\033[0m
1164 | [0] 字体文件输出文件夹: \033[1;33m{6}\033[0m
1165 | '''.format(v_subdir, s_subdir, rmAssIn, rmAttach, mkvout, assout,
1166 | fontout, matchStrict, notfont))
1167 | work = 0
1168 | work = os.system('choice /M 请输入 /C AB1234567890L')
1169 | if work == 2 and no_mkvm:
1170 | print('[ERROR] 在您的系统中找不到 mkvmerge, 该功能不可用')
1171 | work = -1
1172 | if work in [1, 2]:
1173 | cls()
1174 | if work == 1: print('''子集化字体
1175 |
1176 | 搜索子目录(视频): \033[1;33m{0}\033[0m
1177 | 搜索子目录(字幕): \033[1;33m{1}\033[0m
1178 | 严格字幕匹配: \033[1;33m{2}\033[0m
1179 | 字幕文件输出文件夹: \033[1;33m{3}\033[0m
1180 | 字体文件输出文件夹: \033[1;33m{4}\033[0m
1181 | '''.format(v_subdir, s_subdir, matchStrict, assout, fontout))
1182 | else: print('''子集化字体并封装
1183 |
1184 | 搜索子目录(视频): \033[1;33m{4}\033[0m
1185 | 搜索子目录(字幕): \033[1;33m{5}\033[0m
1186 | 移除内挂字幕: \033[1;33m{0}\033[0m
1187 | 移除原有附件: \033[1;33m{1}\033[0m
1188 | 不封装字体: \033[1;33m{2}\033[0m
1189 | 严格字幕匹配: \033[1;33m{6}\033[0m
1190 | 媒体文件输出文件夹: \033[1;33m{3}\033[0m
1191 | '''.format(rmAssIn, rmAttach, notfont, mkvout, v_subdir, s_subdir, matchStrict))
1192 | cpath = ''
1193 | directout = False
1194 | while not path.exists(cpath) and not directout:
1195 | directout = True
1196 | cpath = input('不输入任何值 直接回车回到上一页面\n请输入文件或目录路径: ').strip('\"')
1197 | if cpath == '': print('没有输入,回到上级菜单')
1198 | elif not path.isabs(cpath): print('\033[1;31m[ERROR] 输入的必须是绝对路径!: \"{0}\"\033[0m'.format(cpath))
1199 | elif not path.exists(cpath): print('\033[1;31m[ERROR] 找不到路径: \"{0}\"\033[0m'.format(cpath))
1200 | elif path.isfile(cpath):
1201 | if path.splitext(cpath)[1][1:] not in extlist:
1202 | print('\033[1;31m[ERROR] 扩展名不正确: \"{0}\"\033[0m'.format(cpath))
1203 | else: directout = False
1204 | elif not path.isdir(cpath):
1205 | print('\033[1;31m[ERROR] 输入的应该是目录或媒体文件!: \"{0}\"\033[0m'.format(cpath))
1206 | else: directout = False
1207 | # print(directout)
1208 | if not directout:
1209 | if path.isfile(cpath):
1210 | medias = [[path.splitext(path.basename(cpath))[0], cpath]]
1211 | cpath = path.dirname(cpath)
1212 | else: medias = getMediaFilelist(cpath)
1213 | # print(medias)
1214 | if not medias is None:
1215 | media_ass = getSubtitles(medias, cpath)
1216 | # print(media_ass)
1217 | for k in media_ass.keys():
1218 | #print(k)
1219 | if assout == '':
1220 | assout_cache = path.join(cpath, 'Subtitles')
1221 | elif assout[0] == '?':
1222 | assout_cache = path.join(cpath, assout[1:])
1223 | else: assout_cache = assout
1224 | if fontout == '':
1225 | fontout_cache = path.join(cpath, 'Fonts')
1226 | elif fontout[0] == '?':
1227 | fontout_cache = path.join(cpath, fontout[1:])
1228 | else: fontout_cache = fontout
1229 | if mkvout == '':
1230 | mkvout_cache = ''
1231 | elif mkvout[0] == '?':
1232 | mkvout_cache = path.join(cpath, mkvout[1:])
1233 | else: mkvout_cache = mkvout
1234 |
1235 | #print([assout_cache, fontout_cache, mkvout_cache])
1236 | if work == 1:
1237 | newasspaths, newfont_name, mkvr = main(font_name, media_ass[k],
1238 | mux = False, outdir=[assout_cache, fontout_cache, mkvout_cache])
1239 | else:
1240 | newasspaths, newfont_name, mkvr = main(font_name, media_ass[k],
1241 | mux = True, outdir=[assout_cache, fontout_cache, mkvout_cache], vpath=k)
1242 |
1243 | for ap in newasspaths:
1244 | if path.exists(ap):
1245 | print('\033[1;32m成功:\033[1m \"{0}\"\033[0m'.format(path.basename(ap)))
1246 | for nf in newfont_name.keys():
1247 | if path.exists(newfont_name[nf][0]):
1248 | if newfont_name[nf][1] is None:
1249 | print('\033[1;31m失败:\033[1m \"{0}\"\033[0m >> \033[1m\"{1}\"\033[0m'.format(path.basename(nf), path.basename(newfont_name[nf][0])))
1250 | else:
1251 | print('\033[1;32m成功:\033[1m \"{0}\"\033[0m >> \033[1m\"{1}\" ({2})\033[0m'.format(path.basename(nf), path.basename(newfont_name[nf][0]), newfont_name[nf][1]))
1252 | elif work == 3:
1253 | cls()
1254 | print('ExtList Viewer 1.00-Final\n')
1255 | for i in range(0, len(extlist)):
1256 | s = extlist[i]
1257 | print('[Ext{0:>3d}] {1}'.format(i, s))
1258 | print('\n')
1259 | if not no_mkvm:
1260 | os.system('pause')
1261 | cls()
1262 | os.system('mkvmerge --list-languages')
1263 | #print('\033[1m mkvmerge 语言编码列表 \033[0m')
1264 | else:
1265 | print('没有检测到mkvmerge,无法输出语言编码列表')
1266 | elif work == 4:
1267 | if v_subdir: v_subdir = False
1268 | else: v_subdir = True
1269 | elif work == 5:
1270 | if s_subdir: s_subdir = False
1271 | else: s_subdir = True
1272 | elif work == 6:
1273 | if rmAssIn: rmAssIn = False
1274 | else: rmAssIn = True
1275 | elif work == 7:
1276 | if rmAttach: rmAttach = False
1277 | else: rmAttach = True
1278 | elif work == 8:
1279 | if notfont: notfont = False
1280 | else: notfont = True
1281 | elif work == 9:
1282 | if matchStrict: matchStrict = False
1283 | else: matchStrict = True
1284 | elif work in [10, 11, 12]:
1285 | cls()
1286 | print('''请输入目标目录路径或子目录名称\n(若要输入子目录,请在目录名前加\"?\")
1287 | (不支持多层子目录,会自动将\"\\\"换成下划线)''')
1288 | if work == 10:
1289 | mkvout = checkOutPath(input(), mkvout)
1290 | elif work == 11:
1291 | assout = checkOutPath(input(), assout)
1292 | elif work == 12:
1293 | fontout = checkOutPath(input(), fontout)
1294 | else:
1295 | leave = False
1296 | if work < 4:
1297 | os.system('pause')
1298 |
1299 | def cLicense():
1300 | cls()
1301 | print('''AddSubFontMKV Python Remake 1.00
1302 | Apache-2.0 License
1303 | Copyright(c) 2022 yyfll
1304 |
1305 | 依赖:
1306 | fontTools | MIT License
1307 | chardet | LGPL-2.1 License
1308 | colorama | BSD-3 License
1309 | mkvmerge | GPL-2 License
1310 | ''')
1311 | print('for more information:\nhttps://www.apache.org/licenses/')
1312 | os.system('pause')
1313 |
1314 | cls()
1315 |
1316 | if os.system('choice /? 1>nul 2>nul') > 0:
1317 | no_cmdc = True
1318 |
1319 | if __name__=="__main__":
1320 | while 1:
1321 | work = 0
1322 | print('''ASFMKV Python Remake 1.00 | (c) 2022 yyfll{0}
1323 | 字体名称数: [\033[1;33m{2}\033[0m](包含乱码的名称)
1324 |
1325 | 请选择功能:
1326 | [A] ListAssFont
1327 | [B] 字体子集化 & MKV封装
1328 | [C] 检视重复字体: 重复名称[\033[1;33m{1}\033[0m]
1329 |
1330 | 其他:
1331 | [D] 依赖与许可证
1332 | [L] 直接退出'''.format(mkvmv, len(dupfont.keys()), len(font_name.keys())))
1333 | print('')
1334 | work = os.system('choice /M 请选择: /C ABCDL')
1335 | if work == 1:
1336 | cListAssFont(font_name)
1337 | elif work == 2:
1338 | cFontSubset(font_name)
1339 | elif work == 3:
1340 | cls()
1341 | if len(dupfont.keys()) > 0:
1342 | for s in dupfont.keys():
1343 | print('\033[1;31m[{0}]\033[0m'.format(s))
1344 | for ss in dupfont[s]:
1345 | print('\"{0}\"'.format(ss))
1346 | print('')
1347 | else:
1348 | print('没有名称重复的字体')
1349 | os.system('pause')
1350 | elif work == 4:
1351 | cLicense()
1352 | elif work == 5:
1353 | exit()
1354 | else: exit()
1355 | cls()
1356 |
--------------------------------------------------------------------------------
/ASFMKV_py1.01.py:
--------------------------------------------------------------------------------
1 | # -*- coding: UTF-8 -*-
2 | # *************************************************************************
3 | #
4 | # 请使用支持 UTF-8 NoBOM 并最好带有 Python 语法高亮的文本编辑器
5 | # Windows 7 的用户请最好不要使用 写字板/记事本 打开本脚本
6 | #
7 | # *************************************************************************
8 |
9 | # 调用库,请不要修改
10 | import shutil
11 | from fontTools import ttLib
12 | from fontTools import subset
13 | from chardet.universaldetector import UniversalDetector
14 | import os
15 | from os import path
16 | import sys
17 | import re
18 | import winreg
19 | import zlib
20 | import json
21 | from colorama import init
22 | from datetime import datetime
23 |
24 | # 初始化环境变量
25 | # *************************************************************************
26 | # 自定义变量
27 | # 修改注意: Python的布尔类型首字母要大写 True 或 False,在名称中有单引号的,需要输入反斜杠转义 \'
28 | # *************************************************************************
29 | # extlist 可输入的视频媒体文件扩展名
30 | extlist = 'mkv;mp4;mts;mpg;flv;mpeg;m2ts;avi;webm;rm;rmvb;mov;mk3d;vob'
31 | # *************************************************************************
32 | # mkvout 媒体文件输出目录(封装)
33 | # 在最前方用"?"标记来表示这是一个子目录
34 | # 注意: 在Python中需要在左侧引号前加 r 来保留 Windows 路径中的反斜杠,路径末尾不需要反斜杠
35 | mkvout = ''
36 | # *************************************************************************
37 | # assout 字幕文件输出目录
38 | # 在最前方用"?"标记来表示这是一个子目录
39 | # 注意: 在Python中需要在左侧引号前加 r 来保留 Windows 路径中的反斜杠,路径末尾不需要反斜杠
40 | assout = '?subs'
41 | # *************************************************************************
42 | # fontout 字体文件输出目录
43 | # 在最前方用"?"标记来表示这是一个子目录
44 | # 注意: 在Python中需要在左侧引号前加 r 来保留 Windows 路径中的反斜杠,路径末尾不需要反斜杠
45 | fontout = '?Fonts'
46 | # *************************************************************************
47 | # fontin 自定义字体文件夹,可做额外字体源,必须是绝对路径
48 | # 可以有多个路径,路径之间用"?"来分隔
49 | # 注意: 在Python中需要在左侧引号前加 r 来保留 Windows 路径中的反斜杠,路径末尾不需要反斜杠
50 | fontin = r''
51 | # *************************************************************************
52 | # notfont (封装)字体嵌入
53 | # True 始终嵌入字体
54 | # False 不嵌入字体,不子集化字体,不替换字幕中的字体信息
55 | notfont = False
56 | # *************************************************************************
57 | # sublang (封装)字幕语言
58 | # 会按照您所输入的顺序给字幕赋予语言编码,如果字幕数多于语言数,多出部分将赋予最后一种语言编码
59 | # IDX+SUB 的 DVDSUB 由于 IDX 文件一般有语言信息,对于DVDSUB不再添加语言编码
60 | # 各语言之间应使用半角分号 ; 隔开,如 'chi;chi;chi;und'
61 | # 可以在 mkvmerge -l 了解语言编码
62 | # 可以只有一个 # 号让程序在运行时再询问您,如 '#'
63 | sublang = ''
64 | # *************************************************************************
65 | # matchStrict 严格匹配
66 | # True 媒体文件名必须在字幕文件名的最前方,如'test.mkv'的字幕可以是'test.ass'或是'test.sub.ass',但不能是'sub.test.ass'
67 | # False 只要字幕文件名中有媒体文件名就行了,不管它在哪
68 | matchStrict = True
69 | # *************************************************************************
70 | # rmAssIn (封装)如果输入文件是mkv,删除mkv文件中原有的字幕
71 | rmAssIn = True
72 | # *************************************************************************
73 | # rmAttach (封装)如果输入文件是mkv,删除mkv文件中原有的附件
74 | rmAttach = True
75 | # *************************************************************************
76 | # v_subdir 视频的子目录搜索
77 | v_subdir = False
78 | # *************************************************************************
79 | # s_subdir 字幕的子目录搜索
80 | s_subdir = False
81 | # *************************************************************************
82 | # copyfont (ListAssFont)拷贝字体到源文件夹
83 | copyfont = False
84 | # *************************************************************************
85 | # resultw (ListAssFont)打印结果到源文件夹
86 | resultw = False
87 | # *************************************************************************
88 |
89 | # 以下变量谨慎更改
90 | # subext 可输入的字幕扩展名,按照python列表语法
91 | subext = ['ass', 'ssa', 'srt', 'sup', 'idx']
92 |
93 |
94 | # 以下环境变量不应更改
95 | # 编译 style行 搜索用正则表达式
96 | style_read = re.compile('.*\nStyle:.*')
97 | cillegal = re.compile(r'[\\/:\*"><\|]')
98 | # 切分extlist列表
99 | extlist = [s.strip(' ').lstrip('.').lower() for s in extlist.split(';') if len(s) > 0]
100 | # 切分sublang列表
101 | sublang = [s.strip(' ').lower() for s in sublang.split(';') if len(s) > 0]
102 | # 切分fontin列表
103 | fontin = [s.strip(' ') for s in fontin.split('?') if len(s) > 0]
104 | fontin = [s for s in fontin if path.isdir(s)]
105 | langlist = []
106 | extsupp = []
107 | dupfont = {}
108 | init()
109 |
110 | def fontlistAdd(s: str, fn: str, fontlist: dict) -> dict:
111 | if len(s) > 0:
112 | si = 0
113 | fn = fn.lstrip('@')
114 | if fontlist.get(fn) is None:
115 | fontlist[fn] = s[0]
116 | si += 1
117 | for i in range(si, len(s)):
118 | if not s[i] in fontlist[fn]:
119 | fontlist[fn] = fontlist[fn] + s[i]
120 | return fontlist
121 |
122 | # ASS分析部分
123 | # 需要输入
124 | # asspath: ASS文件的绝对路径
125 | # 可选输入
126 | # fontlist: 可以更新fontlist,用于多ASS同时输入的情况,结构见下
127 | # onlycheck: 只确认字幕中的字体,仅仅返回fontlist
128 | # 将会返回
129 | # fullass: 完整的ASS文件内容,以行分割
130 | # fontlist: 字体与其所需字符 { 字体 : 字符串 }
131 | # styleline: 样式内容的起始行
132 | # font_pos: 字体在样式中的位置
133 | # fn_lines: 带有fn标签的行数与该行的完整特效标签,一项一个 [ [行数, 标签1, 标签2], ... ]
134 | def assAnalyze(asspath: str, fontlist: dict = {}, onlycheck: bool = False):
135 | global style_read
136 | # 初始化变量
137 | eventline = 0
138 | style_pos = 0
139 | style_pos2 = 0
140 | text_pos = 0
141 | font_pos = 0
142 | styleline = 0
143 | fn_lines = []
144 | # 编译分析用正则表达式
145 | event_read = re.compile('.*\nDialogue:.*')
146 | style = re.compile(r'^\[V4.*Styles\]$')
147 | event = re.compile(r'^\[Events\]$')
148 | # 识别文本编码并读取整个SubtitleStationAlpha文件到内存
149 | print('\033[1;33m正在分析字幕: \033[1;37m\"{0}\"\033[0m'.format(path.basename(asspath)))
150 | ass = open(asspath, mode='rb')
151 | # if path.getsize(asspath) <= 100 * 1024:
152 | # ass_b = ass.read()
153 | # ass_code = chardet.detect(ass_b)['encoding'].lower()
154 | # else:
155 | detector = UniversalDetector()
156 | for dt in ass:
157 | detector.feed(dt)
158 | if detector.done:
159 | ass_code = detector.result['encoding']
160 | break
161 | detector.reset()
162 | ass.close()
163 | ass = open(asspath, encoding=ass_code, mode='r')
164 | fullass = ass.readlines()
165 | ass.close()
166 | asslen = len(fullass)
167 | # 在文件中搜索Styles标签和Events标签来确认起始行
168 | for s in range(0, asslen):
169 | if re.match(style, fullass[s]) is not None:
170 | styleline = s
171 | elif re.match(event, fullass[s]) is not None:
172 | eventline = s
173 | if styleline != 0 and eventline != 0:
174 | break
175 |
176 | # 获取Style的 Format 行,并用半角逗号分割
177 | style_format = ''.join(fullass[styleline + 1].split(':')[1:]).strip(' ').split(',')
178 | # 确定Style中 Name 和 Fontname 的位置
179 | for i in range(0, len(style_format)):
180 | if style_format[i].lower().strip(' ').replace('\n', '') == 'name':
181 | style_pos = i
182 | elif style_format[i].lower().strip(' ').replace('\n', '') == 'fontname':
183 | font_pos = i
184 | if style_pos != 0 and font_pos != 0:
185 | break
186 |
187 | # 获取 字体表 与 样式字体对应表
188 | style_font = {}
189 | # style_font 词典内容:
190 | # { 样式 : 字体名 }
191 | # fontlist 词典内容:
192 | # { 字体名 : 使用该字体的文本 }
193 | for i in range(styleline + 2, asslen):
194 | if len(fullass[i].split(':')) < 2:
195 | if i + 1 > asslen:
196 | break
197 | else:
198 | if re.search(style_read, '\n'.join(fullass[i + 1:])) is None:
199 | break
200 | else:
201 | continue
202 | styleStr = ''.join(fullass[i].split(':')[1:]).strip(' ').split(',')
203 | font_key = styleStr[font_pos].lstrip('@')
204 | fontlist.setdefault(font_key, '')
205 | style_font[styleStr[style_pos]] = styleStr[font_pos]
206 |
207 | #print(fontlist)
208 |
209 | # 提取Event的 Format 行,并用半角逗号分割
210 | event_format = ''.join(fullass[eventline + 1].split(':')[1:]).strip(' ').split(',')
211 | # 确定Event中 Style 和 Text 的位置
212 | for i in range(0, len(event_format)):
213 | if event_format[i].lower().replace('\n', '').strip(' ') == 'style':
214 | style_pos2 = i
215 | elif event_format[i].lower().replace('\n', '').strip(' ') == 'text':
216 | text_pos = i
217 | if style_pos2 != 0 and text_pos != 0:
218 | break
219 |
220 | # 获取 字体的字符集
221 | # 先获取 Style,用style_font词典查找对应的 Font
222 | # 再将字符串追加到 fontlist 中对应 Font 的值中
223 | for i in range(eventline + 2, asslen):
224 | eventline_sp = fullass[i].split(':')
225 | if len(eventline_sp) < 2:
226 | if i + 1 > asslen:
227 | break
228 | else:
229 | if re.search(event_read, '\n'.join(fullass[i + 1:])) is None:
230 | break
231 | else:
232 | continue
233 | #print(fullass[i])
234 | if eventline_sp[0].strip(' ').lower() == 'comment':
235 | continue
236 | eventline_sp = ''.join(eventline_sp[1:]).split(',')
237 | eventftext = ','.join(eventline_sp[text_pos:])
238 | effectDel = r'(\{.*?\})|(\\[hnN])|(\s)|(.*m .*\w.*)'
239 | vecpos = []
240 | effpos = []
241 | for s in re.findall(r'{\\.*?}', eventftext):
242 | if re.search(r'\\p[1-9][0-9]*', s) is not None:
243 | vecpos.append(s)
244 | else: effpos.append(s)
245 | textremain = ''
246 | lastend = 0
247 | if len(vecpos) > 0:
248 | for v in vecpos:
249 | etext = re.sub(effectDel, '', eventftext[lastend:lastend + eventftext.find(v)].strip(' '))
250 | # print(v)
251 | lastend = eventftext.find(v) + len(v)
252 | endvec = re.search(r'\\p0', eventftext[lastend:])
253 | rmvecr = r'\\p[0-9]+'
254 | rmvec = re.sub(rmvecr, '', eventftext[eventftext.find(v):lastend])
255 | textremain = textremain + etext + rmvec
256 | # print(1, textremain)
257 | if endvec is not None:
258 | endvec = endvec.span()
259 | for s in effpos:
260 | endpos = eventftext.find(s)
261 | nextpos = endpos + len(effpos)
262 | if endpos >= endvec[0] and nextpos <= endvec[1]:
263 | endvecp = eventftext[nextpos:]
264 | if len(endvecp) > 0:
265 | textremain = textremain + re.sub(rmvecr, '', endvecp)
266 | lastend += endvecp
267 | else:
268 | break
269 | # print(2, textremain)
270 | eventftext = eventftext[lastend:]
271 | if len(textremain) > 0:
272 | eventftext = textremain
273 | eventfont = style_font.get(eventline_sp[style_pos2].lstrip('*'))
274 | # print(eventftext)
275 | #fneffect = re.findall(r'{\\fn.*?}', eventftext)
276 | it = re.search(r'{\\fn.*?}', eventftext)
277 | if it is not None:
278 | fn = ''
279 | fn_line = [i]
280 | while it is not None:
281 | it = it.span()
282 | if it[0] > 0:
283 | s = re.sub(effectDel, '', eventftext[:it[0]])
284 | # print('add', s)
285 | # print('fn', fn)
286 | if len(fn) > 0:
287 | fontlist = fontlistAdd(s, fn, fontlist)
288 | else: fontlist = fontlistAdd(s, eventfont, fontlist)
289 | s = eventftext[(it[0] + 1):(it[1] - 1)]
290 | l = [sf.strip(' ') for sf in s.split('\\') if len(s.strip(' ')) > 0]
291 | l.reverse()
292 | for sf in l:
293 | if 'fn' in sf.lower():
294 | fn = sf[2:].strip(' ').lstrip('@')
295 | fn_line.append(s)
296 | break
297 | eventftext = eventftext[it[1]:]
298 | # print('ef', eventftext)
299 | it = re.search(r'{\\fn.*?}', eventftext)
300 | # os.system('pause')
301 | else:
302 | s = re.sub(effectDel, '', eventftext)
303 | # print('add', s)
304 | # print('fn', fn)
305 | if len(fn) > 0:
306 | fontlist = fontlistAdd(s, fn, fontlist)
307 | else: fontlist = fontlistAdd(s, eventfont, fontlist)
308 | fn_lines.append(fn_line)
309 | else:
310 | if not eventfont is None:
311 | # 去除行中非文本部分,包括特效标签{},硬软换行符
312 | eventtext = re.sub(r'(\{.*?\})|(\\[hnN])|(\s)', '', eventftext)
313 | #print(eventfont, eventtext)
314 | #print(eventtext, ','.join(eventline_sp[text_pos:]))
315 | fontlist = fontlistAdd(eventtext, eventfont, fontlist)
316 |
317 | if not onlycheck: print('\033[1m字幕所需字体\033[0m')
318 | fl_popkey = []
319 | # 在字体列表中检查是否有没有在文本中使用的字体,如果有,添加到删去列表
320 | for s in fontlist.keys():
321 | if len(fontlist[s]) == 0:
322 | fl_popkey.append(s)
323 | #print('跳过没有字符的字体\"{0}\"'.format(s))
324 | else:
325 | if not onlycheck: print('\033[1m\"{0}\"\033[0m: 字符数[\033[1;33m{1}\033[0m]'.format(s, len(fontlist[s])))
326 | # 删去 删去列表 中的字体
327 | if len(fl_popkey) > 0:
328 | for s in fl_popkey:
329 | fontlist.pop(s)
330 |
331 | # 如果 onlycheck 为 True,只返回字体列表
332 | if onlycheck:
333 | del fullass
334 | style_font.clear()
335 | return None, fontlist, None, None, None
336 |
337 | #os.system('pause')
338 | return fullass, fontlist, styleline, font_pos, fn_lines
339 |
340 | # 获取字体文件列表
341 | # 接受输入
342 | # customPath: 用户指定的字体文件夹
343 | # font_name: 用于更新font_name(启用从注册表读取名称的功能时有效)
344 | # noreg: 只从用户提供的customPath获取输入
345 | # 将会返回
346 | # filelist: 字体文件清单 [[ 字体绝对路径, 读取位置('': 注册表, '0': 自定义目录) ], ...]
347 | # font_name: 用于更新font_name(启用从注册表读取名称的功能时有效)
348 | def getFileList(customPath: list = [], font_name: dict = {}, noreg: bool = False):
349 | filelist = []
350 |
351 | if not noreg:
352 | # 从注册表读取
353 | fontkey = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, r'SOFTWARE\Microsoft\Windows NT\CurrentVersion\Fonts')
354 | fontkey_num = winreg.QueryInfoKey(fontkey)[1]
355 | #fkey = ''
356 | try:
357 | # 从用户字体注册表读取
358 | fontkey10 = winreg.OpenKey(winreg.HKEY_CURRENT_USER, r'SOFTWARE\Microsoft\Windows NT\CurrentVersion\Fonts')
359 | fontkey10_num = winreg.QueryInfoKey(fontkey10)[1]
360 | if fontkey10_num > 0:
361 | for i in range(fontkey10_num):
362 | p = winreg.EnumValue(fontkey10, i)[1]
363 | #n = winreg.EnumValue(fontkey10, i)[0]
364 | if path.exists(p):
365 | # test = n.split('&')
366 | # if len(test) > 1:
367 | # for i in range(0, len(test)):
368 | # font_name[re.sub(r'\(.*?\)', '', test[i].strip(' '))] = [p, i]
369 | # else: font_name[re.sub(r'\(.*?\)', '', n.strip(' '))] = [p, 0]
370 | filelist.append([p, ''])
371 | # test = path.splitext(path.basename(p))[0].split('&')
372 | except:
373 | pass
374 | for i in range(fontkey_num):
375 | # 从 系统字体注册表 读取
376 | k = winreg.EnumValue(fontkey, i)[1]
377 | #n = winreg.EnumValue(fontkey, i)[0]
378 | pk = path.join(r'C:\Windows\Fonts', k)
379 | if path.exists(pk):
380 | # test = n.split('&')
381 | # if len(test) > 1:
382 | # for i in range(0, len(test)):
383 | # font_name[re.sub(r'\(.*?\)', '', test[i].strip(' '))] = [pk, i]
384 | # else: font_name[re.sub(r'\(.*?\)', '', n.strip(' '))] = [pk, 0]
385 | filelist.append([pk, ''])
386 |
387 | # 从定义的文件夹读取
388 | # fontspath = [r'C:\Windows\Fonts', path.join(os.getenv('USERPROFILE'),r'AppData\Local\Microsoft\Windows\Fonts')]
389 | if customPath is None: customPath == []
390 | if len(customPath) > 0:
391 | print('\033[1;33m请稍等,正在获取自定义文件夹中的字体\033[0m')
392 | for s in customPath:
393 | if not path.isdir(s): continue
394 | for r, d, f in os.walk(s):
395 | for p in f:
396 | p = path.join(r, p)
397 | if path.splitext(p)[1][1:].lower() not in ['ttf', 'ttc', 'otc', 'otf']: continue
398 | filelist.append([path.join(s, p), 'xxx'])
399 |
400 | #print(font_name)
401 | #os.system('pause')
402 | return filelist, font_name
403 |
404 | #字体处理部分
405 | # 需要输入
406 | # fl: 字体文件列表
407 | # 可选输入
408 | # f_n: 默认新建一个,可用于更新font_name
409 | # 将会返回
410 | # font_name: 字体内部名称与绝对路径的索引词典
411 | # 会对以下全局变量进行变更
412 | # dupfont: 重复字体的名称与其路径词典
413 |
414 | # font_name 词典结构
415 | # { 字体名称 : [ 字体绝对路径 , 字体索引 (仅用于TTC/OTC; 如果是TTF/OTF,默认为0) ] }
416 | # dupfont 词典结构
417 | # { 重复字体名称 : [ 字体1绝对路径, 字体2绝对路径, ... ] }
418 | def fontProgress(fl: list, f_n: dict = {}) -> dict:
419 | global dupfont
420 | #print(fl)
421 | flL = len(fl)
422 | print('\033[1;32m正在读取字体信息...\033[0m')
423 | for si in range(0, flL):
424 | s = fl[si][0]
425 | fromCustom = False
426 | if len(fl[si][1]) > 0: fromCustom = True
427 | # 如果有来自自定义文件夹标记,则将 fromCustom 设为 True
428 | ext = path.splitext(s)[1][1:]
429 | # 检查字体扩展名
430 | if ext.lower() not in ['ttf','ttc','otf','otc']: continue
431 | # 输出进度
432 | print('\r' + '\033[1;32m{0}/{1} {2:.2f}% \033[0m'.format(si + 1, flL, ((si + 1)/flL)*100, ), end='', flush=True)
433 | if ext.lower() in ['ttf', 'otf']:
434 | # 如果是 TTF/OTF 单字体文件,则使用 TTFont 读取
435 | try:
436 | tc = [ttLib.TTFont(s, lazy=True)]
437 | except:
438 | print('\033[1;31m\n[ERROR] \"{0}\": {1}\n\033[1;34m[TRY] 正在尝试使用TTC/OTC模式读取\033[0m'.format(s, sys.exc_info()))
439 | # 如果 TTFont 读取失败,可能是使用了错误扩展名的 TTC/OTC 文件,换成 TTCollection 尝试读取
440 | try:
441 | tc = ttLib.TTCollection(s, lazy=True)
442 | print('\033[1;34m[WARNING] 错误的字体扩展名\"{0}\" \033[0m'.format(s))
443 | except:
444 | print('\033[1;31m\n[ERROR] \"{0}\": {1}\033[0m'.format(s, sys.exc_info()))
445 | continue
446 | else:
447 | try:
448 | # 如果是 TTC/OTC 字体集合文件,则使用 TTCollection 读取
449 | tc = ttLib.TTCollection(s, lazy=True)
450 | except:
451 | print('\033[1;31m\n[ERROR] \"{0}\": {1}\n\033[1;34m[TRY] 正在尝试使用TTF/OTF模式读取\033[0m'.format(s, sys.exc_info()))
452 | try:
453 | # 如果读取失败,可能是使用了错误扩展名的 TTF/OTF 文件,用 TTFont 尝试读取
454 | tc = [ttLib.TTFont(s, lazy=True)]
455 | print('\033[1;34m[WARNING] 错误的字体扩展名\"{0}\" \033[0m'.format(s))
456 | except:
457 | print('\033[1;31m\n[ERROR] \"{0}\": {1}\033[0m'.format(s, sys.exc_info()))
458 | continue
459 | #f_n[path.splitext(path.basename(s))[0]] = [s, 0]
460 | for ti in range(0, len(tc)):
461 | t = tc[ti]
462 | # 读取字体的 'name' 表
463 | for ii in range(0, len(t['name'].names)):
464 | name = t['name'].names[ii]
465 | # 若 nameID 为 4,读取 NameRecord 的文本信息
466 | if name.nameID == 4:
467 | namestr = ''
468 | try:
469 | namestr = name.toStr()
470 | except:
471 | # 如果 fontTools 解码失败,则尝试使用 utf-16-be 直接解码
472 | namestr = name.toBytes().decode('utf-16-be', errors='ignore')
473 | try:
474 | # 尝试使用 去除 \x00 字符 解码
475 | if len([i for i in name.toBytes() if i == 0]) > 0:
476 | nnames = t['name']
477 | namebyte = b''.join([bytes.fromhex('{:0>2}'.format(hex(i)[2:])) for i in name.toBytes() if i > 0])
478 | nnames.setName(namebyte,
479 | name.nameID, name.platformID, name.platEncID, name.langID)
480 | namestr = nnames.names[ii].toStr()
481 | print('\n\033[1;33m已修正字体\"{0}\"名称读取 >> \"{1}\"\033[0m'.format(path.basename(s), namestr))
482 | #os.system('pause')
483 | else: namestr = name.toBytes().decode('utf-16-be', errors='ignore')
484 | # 如果没有 \x00 字符,使用 utf-16-be 强行解码;如果有,尝试解码;如果解码失败,使用 utf-16-be 强行解码
485 | except:
486 | print('\n\033[1;33m尝试修正字体\"{0}\"名称读取 >> \"{1}\"\033[0m'.format(path.basename(s), namestr))
487 | if namestr is None: continue
488 | namestr = namestr.strip(' ')
489 | #print(namestr, path.basename(s))
490 | if f_n.get(namestr) is not None:
491 | # 如果发现列表中已有相同名称的字体,检测它的文件名、扩展名、父目录是否相同
492 | # 如果有一者不同且不来自自定义文件夹,添加到重复字体列表
493 | dupp = f_n[namestr][0]
494 | if dupp != s and path.splitext(path.basename(dupp))[0] != path.splitext(path.basename(s))[0] and not fromCustom:
495 | print('\033[1;35m[WARNING] 字体\"{0}\"与字体\"{1}\"的名称\"{2}\"重复!\033[0m'.format(path.basename(f_n[namestr][0]), path.basename(s), namestr))
496 | if dupfont.get(namestr) is not None:
497 | if s not in dupfont[namestr]:
498 | dupfont[namestr].append(s)
499 | else:
500 | dupfont[namestr] = [dupp, s]
501 | else: f_n[namestr] = [s, ti]
502 | #f_n[namestr] = [s, ti]
503 | # if f_n.get(fname) is None: f_n[fname] = [[namestr], s]
504 | # #print(fname, name.toStr(), f_n.get(fname))
505 | # if namestr not in f_n[fname][0]:
506 | # f_n[fname][0] = f_n[fname][0] + [namestr]
507 | tc[0].close()
508 | return f_n
509 |
510 | #print(filelist)
511 | #if path.exists(fontspath10): filelist = filelist.extend(os.listdir(fontspath10))
512 |
513 | #for s in font_name.keys(): print('{0}: {1}'.format(s, font_name[s]))
514 |
515 | # 系统字体完整性检查,检查是否有ASS所需的全部字体,如果没有,则要求拖入
516 | # 需要以下输入
517 | # fontlist: 字体与其所需字符(只读字体部分) { ASS内的字体名称 : 字符串 }
518 | # font_name: 字体名称与字体路径对应词典 { 字体名称 : [ 字体绝对路径, 字体索引 ] }
519 | # 以下输入可选
520 | # assfont: 结构见下,用于多ASS文件时更新列表
521 | # onlycheck: 缺少字体时不要求输入
522 | # 将会返回以下
523 | # assfont: { 字体绝对路径?字体索引 : [ 字符串, ASS内的字体名称 ]}
524 | # font_name: 同上,用于有新字体拖入时对该词典的更新
525 | def checkAssFont(fontlist: dict, font_name: dict, assfont: dict = {}, onlycheck: bool = False):
526 | # 从fontlist获取字体名称
527 | for s in fontlist.keys():
528 | cok = False
529 | # 在全局字体名称词典中寻找字体名称
530 | if s not in font_name:
531 | # 如果找不到,将字体名称统一为小写再次查找
532 | font_name_cache = {}
533 | for ss in font_name.keys():
534 | if ss.lower() == s.lower():
535 | font_name_cache[s] = font_name[ss]
536 | cok = True
537 | break
538 | # update字体名称词典
539 | font_name.update(font_name_cache)
540 | else: cok = True
541 | directout = 0
542 | if not cok:
543 | # 如果 onlycheck 不为 True,向用户要求目标字体
544 | if not onlycheck:
545 | print('\033[1;31m[ERROR] 缺少字体\"{0}\"\n请输入追加的字体文件或其所在字体目录的绝对路径\033[0m'.format(s))
546 | addFont = {}
547 | inFont = ''
548 | while inFont == '' and directout < 3:
549 | inFont = input().strip('\"').strip(' ')
550 | if path.exists(inFont):
551 | if path.isdir(inFont):
552 | addFont = fontProgress(getFileList([inFont], noreg=True)[0])
553 | else:
554 | addFont = fontProgress([[inFont, '0']])
555 | if s not in addFont.keys():
556 | if path.isdir(inFont):
557 | print('\033[1;31m[ERROR] 输入路径中\"{0}\"没有所需字体\"{1}\"\033[0m'.format(inFont, s))
558 | else: print('\033[1;31m[ERROR] 输入字体\"{0}\"不是所需字体\"{1}\"\033[0m'.format('|'.join(addFont.keys()), s))
559 | inFont = ''
560 | else:
561 | font_name.update(addFont)
562 | cok = True
563 | else:
564 | print('\033[1;31m[ERROR] 您没有输入任何字符!再回车{0}次回到主菜单\033[0m'.format(3-directout))
565 | directout += 1
566 | inFont = ''
567 | else:
568 | # 否则直接添加空
569 | assfont['?'.join([s, s])] = ['', s]
570 | if cok and directout < 3:
571 | # 如果找到,添加到assfont列表
572 | font_path = font_name[s][0]
573 | font_index = font_name[s][1]
574 | dict_key = '?'.join([font_path, str(font_index)])
575 | # 如果 assfont 列表已有该字体,则将新字符添加到 assfont 中
576 | if assfont.get(dict_key) is None:
577 | assfont[dict_key] = [fontlist[s], s]
578 | else:
579 | if assfont[dict_key][1] == font_index:
580 | newfname = assfont[dict_key][2]
581 | if s != newfname:
582 | newfname = '|'.join([s, newfname])
583 | newstr = assfont[dict_key][1]
584 | newstr2 = ''
585 | for i in range(0, len(newstr)):
586 | if newstr[i] not in fontlist[s]:
587 | newstr2 = newstr2 + newstr[i]
588 | assfont[dict_key] = [fontlist[s] + newstr2, newfname]
589 | else:
590 | assfont[dict_key] = [fontlist[s], s]
591 | #print(assfont[dict_key])
592 | elif directout >= 3:
593 | return None, font_name
594 |
595 | return assfont, font_name
596 |
597 | # print('正在输出字体子集字符集')
598 | # for s in fontlist.keys():
599 | # logpath = '{0}_{1}.log'.format(path.join(os.getenv('TEMP'), path.splitext(path.basename(asspath))[0]), s)
600 | # log = open(logpath, mode='w', encoding='utf-8')
601 | # log.write(fontlist[s])
602 | # log.close()
603 |
604 | # 字体内部名称变更
605 | def getNameStr(name, subfontcrc: str) -> str:
606 | namestr = ''
607 | nameID = name.nameID
608 | # 变更NameID为1, 3, 4, 6的NameRecord,它们分别对应
609 | # ID Meaning
610 | # 1 Font Family name
611 | # 3 Unique font identifier
612 | # 4 Full font name
613 | # 6 PostScript name for the font
614 | # 注意本脚本并不更改 NameID 为 0 和 7 的版权信息
615 | if nameID in [1,3,4,6]:
616 | namestr = subfontcrc
617 | else:
618 | try:
619 | namestr = name.toStr()
620 | except:
621 | namestr = name.toBytes().decode('utf-16-be', errors='ignore')
622 | return namestr
623 |
624 | # 字体子集化
625 | # 需要以下输入:
626 | # assfont: { 字体绝对路径?字体索引 : [ 字符串, ASS内的字体名称 ]}
627 | # fontdir: 新字体存放目录
628 | # 将会返回以下:
629 | # newfont_name: { 原字体名 : [ 新字体绝对路径, 新字体名 ] }
630 | def assFontSubset(assfont: dict, fontdir: str) -> dict:
631 | newfont_name = {}
632 | # print(fontdir)
633 |
634 | if path.exists(path.dirname(fontdir)):
635 | if not path.isdir(fontdir):
636 | try:
637 | os.mkdir(fontdir)
638 | except:
639 | print('\033[1;31m[ERROR] 创建文件夹\"{0}\"失败\033[0m'.format(fontdir))
640 | fontdir = os.getcwd()
641 | if not path.isdir(fontdir): fontdir = path.dirname(fontdir)
642 | else: fontdir = os.getcwd()
643 | print('\033[1;33m字体输出路径:\033[0m \033[1m\"{0}\"\033[0m'.format(fontdir))
644 |
645 | lk = len(assfont.keys())
646 | kip = 0
647 | for k in assfont.keys():
648 | kip += 1
649 | # 偷懒没有变更该函数中的assfont解析到新的词典格式
650 | # 在这里会将assfont词典转换为旧的assfont列表形式
651 | # assfont: [ 字体绝对路径, 字体索引, 字符串, ASS内的字体名称 ]
652 | s = k.split('?') + [assfont[k][0], assfont[k][1]]
653 | subfontext = ''
654 | fontext = path.splitext(path.basename(s[0]))[1]
655 | if fontext[1:].lower() in ['otc', 'ttc']:
656 | subfontext = fontext[:3] + 'f'
657 | else: subfontext = fontext
658 | #print(fontdir, path.exists(path.dirname(fontdir)), path.exists(fontdir))
659 | fontname = re.sub(cillegal, '_', s[3])
660 | subfontpath = path.join(fontdir, fontname + subfontext)
661 | #print(fontdir, subfontpath)
662 | # if not path.exists(path.dirname(subfontpath)):
663 | # try:
664 | # os.mkdir(path.dirname(subfontpath))
665 | # except:
666 | # subfontpath = path.join(fontdir, fontname + subfontext)
667 | # print('\033[1;31m[ERROR] 创建文件夹\"{0}\"失败\033[0m'.format(fontdir))
668 | subsetarg = [s[0], '--text={0}'.format(s[2]), '--output-file={0}'.format(subfontpath), '--font-number={0}'.format(s[1]), '--passthrough-tables']
669 | print('\r\033[1;32m[{0}/{1}]\033[0m \033[1m正在子集化…… \033[0m'.format(kip, lk), end='')
670 | try:
671 | subset.main(subsetarg)
672 | except PermissionError:
673 | print('\n\033[1;31m[ERROR] 文件\"{0}\"访问失败\033[0m'.format(path.basename(subfontpath)))
674 | continue
675 | except:
676 | # print('\033[1;31m[ERROR] 失败字符串: \"{0}\" \033[0m'.format(s[2]))
677 | print('\n\033[1;31m[ERROR] {0}\033[0m'.format(sys.exc_info()))
678 | print('\033[1;31m[WARNING] 字体\"{0}\"子集化失败,将会保留完整字体\033[0m'.format(path.basename(s[0])))
679 | # crcnewf = ''.join([path.splitext(subfontpath)[0], fontext])
680 | # shutil.copy(s[0], crcnewf)
681 | ttLib.TTFont(s[0], lazy=False, fontNumber=int(s[1])).save(subfontpath, False)
682 | subfontcrc = None
683 | # newfont_name[s[3]] = [crcnewf, subfontcrc]
684 | newfont_name[s[3]] = [subfontpath, subfontcrc]
685 | continue
686 | #os.system('pyftsubset {0}'.format(' '.join(subsetarg)))
687 | if path.exists(subfontpath):
688 | subfontbyte = open(subfontpath, mode='rb')
689 | subfontcrc = str(hex(zlib.crc32(subfontbyte.read())))[2:].upper()
690 | if len(subfontcrc) < 8: subfontcrc = '0' + subfontcrc
691 | # print('CRC32: {0} \"{1}\"'.format(subfontcrc, path.basename(s[0])))
692 | subfontbyte.close()
693 | rawf = ttLib.TTFont(s[0], lazy=True, fontNumber=int(s[1]))
694 | newf = ttLib.TTFont(subfontpath, lazy=False)
695 | if len(newf['name'].names) == 0:
696 | for i in range(0,7):
697 | if len(rawf['name'].names) - 1 >= i:
698 | name = rawf['name'].names[i]
699 | namestr = getNameStr(name, subfontcrc)
700 | newf['name'].addName(namestr, minNameID=-1)
701 | else:
702 | for i in range(0, len(rawf['name'].names)):
703 | name = rawf['name'].names[i]
704 | nameID = name.nameID
705 | platID = name.platformID
706 | langID = name.langID
707 | platEncID = name.platEncID
708 | namestr = getNameStr(name, subfontcrc)
709 | newf['name'].setName(namestr ,nameID, platID, platEncID, langID)
710 | if len(newf.getGlyphOrder()) == 1 and '.notdef' in newf.getGlyphOrder():
711 | print('\n\033[1;31m[WARNING] 字体\"{0}\"子集化失败,将会保留完整字体\033[0m'.format(path.basename(s[0])))
712 | crcnewf = subfontpath
713 | newf.close()
714 | if not subfontpath == s[0]: os.remove(subfontpath)
715 | # shutil.copy(s[0], crcnewf)
716 | rawf.save(crcnewf, False)
717 | subfontcrc = None
718 | else:
719 |
720 | crcnewf = '.{0}'.format(subfontcrc).join(path.splitext(subfontpath))
721 | newf.save(crcnewf)
722 | newf.close()
723 | rawf.close()
724 | if path.exists(crcnewf):
725 | if not subfontpath == crcnewf: os.remove(subfontpath)
726 | newfont_name[s[3]] = [crcnewf, subfontcrc]
727 | print('')
728 | #print(newfont_name)
729 | return newfont_name
730 |
731 | # 更改ASS样式对应的字体
732 | # 需要以下输入
733 | # fullass: 完整的ass文件内容,以行分割为列表
734 | # newfont_name: { 原字体名 : [ 新字体路径, 新字体名 ] }
735 | # asspath: 原ass文件的绝对路径
736 | # styleline: [V4/V4+ Styles]标签在SSA/ASS中的行数,对应到fullass列表的索引数
737 | # font_pos: Font参数在 Styles 的 Format 中位于第几个逗号之后
738 | # 以下输入可选
739 | # outdir: 新字幕的输出目录,默认为源文件目录
740 | # ncover: 为True时不覆盖原有文件,为False时覆盖
741 | # fn_lines: 带有fn标签的行数,对应到fullass的索引
742 | # 将会返回以下
743 | # newasspath: 新ass文件的绝对路径
744 | def assFontChange(fullass: list, newfont_name: dict, asspath: str, styleline: int,
745 | font_pos: int, outdir: str = '', ncover: bool = False, fn_lines: list = []) -> str:
746 | # 扫描Style各行,并替换掉字体名称
747 | #print('正在替换style对应字体......')
748 | for i in range(styleline + 2, len(fullass)):
749 | if len(fullass[i].split(':')) < 2:
750 | if re.search(style_read, '\n'.join(fullass[i + 1:])) is None:
751 | break
752 | else:
753 | continue
754 | styleStr = ''.join(fullass[i].split(':')[1:]).strip(' ').split(',')
755 | fontstr = styleStr[font_pos].lstrip('@')
756 | if not newfont_name.get(fontstr) is None:
757 | if not newfont_name[fontstr][1] is None:
758 | fullass[i] = fullass[i].replace(fontstr, newfont_name[fontstr][1])
759 | if len(fn_lines) > 0:
760 | #print('正在处理fn标签......')
761 | for fl in fn_lines:
762 | fn_line = fullass[fl[0]]
763 | for ti in range(1, len(fl)):
764 | for k in newfont_name.keys():
765 | if k in fl[ti]:
766 | fn_line = fn_line.replace(fl[ti], fl[ti].replace(k, newfont_name[k][1]))
767 | fullass[fl[0]] = fn_line
768 | if path.exists(path.dirname(outdir)):
769 | if not path.isdir(outdir):
770 | try:
771 | os.mkdir(outdir)
772 | except:
773 | print('\033[1;31m[ERROR] 创建文件夹\"{0}\"失败\033[0m'.format(outdir))
774 | outdir = os.getcwd()
775 | print('\033[1;33m字幕输出路径:\033[0m \033[1m\"{0}\"\033[0m'.format(outdir))
776 | if path.isdir(outdir):
777 | newasspath = path.join(outdir, '.subset'.join(path.splitext(path.basename(asspath))))
778 | else: newasspath = '.subset'.join(path.splitext(asspath))
779 | if path.exists(newasspath) and ncover:
780 | testpathl = path.splitext(newasspath)
781 | testc = 1
782 | testpath = '{0}#{1}{2}'.format(testpathl[0], testc, testpathl[1])
783 | while path.exists(testpath):
784 | testc += 1
785 | testpath = '{0}#{1}{2}'.format(testpathl[0], testc, testpathl[1])
786 | newasspath = testpath
787 | ass = open(newasspath, mode='w', encoding='utf-8')
788 | ass.writelines(fullass)
789 | ass.close()
790 | #print('ASS样式转换完成: {0}'.format(path.basename(newasspath)))
791 | return newasspath
792 |
793 | # ASFMKV,将媒体文件、字幕、字体封装到一个MKV文件,需要mkvmerge命令行支持
794 | # 需要以下输入
795 | # file: 媒体文件绝对路径
796 | # outfile: 输出文件的绝对路径,如果该选项空缺,默认为 输入媒体文件.muxed.mkv
797 | # asslangs: 赋值给字幕轨道的语言,如果字幕轨道多于asslangs的项目数,超出部分将全部应用asslangs的末项
798 | # asspaths: 字幕绝对路径列表
799 | # fontpaths: 字体列表,格式为 [[字体1绝对路径], [字体1绝对路径], ...],必须嵌套一层,因为主函数偷懒了
800 | # 将会返回以下
801 | # mkvmr: mkvmerge命令行的返回值
802 | def ASFMKV(file: str, outfile: str = '', asslangs: list = [], asspaths: list = [], fontpaths: list = []) -> int:
803 | #print(fontpaths)
804 | global rmAssIn, rmAttach, mkvout, notfont
805 | if file is None: return 4
806 | elif file == '': return 4
807 | elif not path.exists(file) or not path.isfile(file): return 4
808 | if outfile is None: outfile = ''
809 | if outfile == '' or not path.exists(path.dirname(outfile)) or path.dirname(outfile) == path.dirname(file):
810 | outfile = '.muxed'.join([path.splitext(file)[0], '.mkv'])
811 | outfile = path.splitext(outfile)[0] + '.mkv'
812 | if path.exists(outfile):
813 | checkloop = 1
814 | while path.exists('#{0}'.format(checkloop).join(path.splitext(outfile))):
815 | checkloop += 1
816 | outfile = '#{0}'.format(checkloop).join([path.splitext(outfile)[0], '.mkv'])
817 | mkvargs = []
818 | if rmAssIn: mkvargs.append('-S')
819 | if rmAttach: mkvargs.append('-M')
820 | mkvargs.extend(['(', file, ')'])
821 | fn = path.splitext(path.basename(file))[0]
822 | if len(asspaths) > 0:
823 | for i in range(0, len(asspaths)):
824 | s = asspaths[i]
825 | assfn = path.splitext(path.basename(s))[0]
826 | assnote = assfn[(assfn.find(fn) + len(fn)):].replace('.subset', '')
827 | #print(assfn, fn, assnote)
828 | if len(assnote) > 1:
829 | mkvargs.extend(['--track-name', '0:{0}'.format(assnote.lstrip('.'))])
830 | if len(asslangs) > 0 and path.splitext(s)[1][1:].lower() not in ['idx']:
831 | mkvargs.append('--language')
832 | if i < len(asslangs):
833 | mkvargs.append('0:{0}'.format(asslangs[i]))
834 | else:
835 | mkvargs.append('0:{0}'.format(asslangs[len(asslangs) - 1]))
836 | mkvargs.extend(['(', s, ')'])
837 | if len(fontpaths) > 0:
838 | for s in fontpaths:
839 | fext = path.splitext(s[0])[1][1:].lower()
840 | if fext in ['ttf', 'ttc']:
841 | mkvargs.extend(['--attachment-mime-type', 'application/x-truetype-font'])
842 | elif fext in ['otf', 'otc']:
843 | mkvargs.extend(['--attachment-mime-type', 'application/vnd.ms-opentype'])
844 | mkvargs.extend(['--attach-file', s[0]])
845 | mkvargs.extend(['--title', fn])
846 | mkvjsonp = path.splitext(file)[0] + '.mkvmerge.json'
847 | mkvjson = open(mkvjsonp, mode='w', encoding='utf-8')
848 | json.dump(mkvargs, fp=mkvjson, sort_keys=True, indent=2, separators=(',', ': '))
849 | mkvjson.close()
850 | mkvmr = os.system('mkvmerge @\"{0}\" -o \"{1}\"'.format(mkvjsonp, outfile))
851 | if mkvmr > 1:
852 | print('\n\033[1;31m[ERROR] 检测到不正常的mkvmerge返回值,重定向输出...\033[0m')
853 | os.system('mkvmerge -r \"{0}\" @\"{1}\" -o NUL'.format('{0}.{1}.log'
854 | .format(path.splitext(file)[0], datetime.now().strftime('%Y-%m%d-%H%M-%S_%f')), mkvjsonp))
855 | elif not notfont:
856 | for p in asspaths:
857 | print('\033[1;32m封装成功: \033[1;37m\"{0}\"\033[0m'.format(p))
858 | if path.splitext(p)[1][1:].lower() in ['ass', 'ssa']:
859 | try:
860 | os.remove(p)
861 | except:
862 | print('\033[1;33m[ERROR] 文件\"{0}\"删除失败\033[0m'.format(p))
863 | for f in fontpaths:
864 | print('\033[1;32m封装成功: \033[1;37m\"{0}\"\033[0m'.format(f[0]))
865 | try:
866 | os.remove(f[0])
867 | except:
868 | print('\033[1;33m[ERROR] 文件\"{0}\"删除失败\033[0m'.format(f[0]))
869 | try:
870 | os.remove(mkvjsonp)
871 | except:
872 | print('\033[1;33m[ERROR] 文件\"{0}\"删除失败\033[0m'.format(mkvjsonp))
873 | print('\033[1;32m输出成功:\033[0m \033[1m\"{0}\"\033[0m'.format(outfile))
874 | else:
875 | print('\033[1;32m输出成功:\033[0m \033[1m\"{0}\"\033[0m'.format(outfile))
876 | return mkvmr
877 |
878 | # 从输入的目录中获取媒体文件列表
879 | # 需要以下输入
880 | # dir: 要搜索的目录
881 | # 返回以下结果
882 | # medias: 多媒体文件列表
883 | # 结构: [[ 文件名(无扩展名), 绝对路径 ], ...]
884 | def getMediaFilelist(dir: str) -> list:
885 | medias = []
886 | global v_subdir, extlist
887 | if path.isdir(dir):
888 | if v_subdir:
889 | for r,ds,fs in os.walk(dir):
890 | for f in fs:
891 | if path.splitext(f)[1][1:].lower() in extlist:
892 | medias.append([path.splitext(path.basename(f))[0], path.join(r, f)])
893 | else:
894 | for f in os.listdir(dir):
895 | if path.isfile(path.join(dir, f)):
896 | if path.splitext(f)[1][1:].lower() in extlist:
897 | medias.append([path.splitext(path.basename(f))[0], path.join(dir, f)])
898 | return medias
899 |
900 | # 在目录中找到与媒体文件列表中的媒体文件对应的字幕
901 | # 遵循以下原则
902 | # 媒体文件在上级目录,则匹配子目录中的字幕;媒体文件的字幕只能在媒体文件的同一目录或子目录中,不能在上级目录和其他同级目录
903 | # 需要以下输入
904 | # medias: 媒体文件列表,结构见 getMediaFilelist
905 | # cpath: 开始搜索的顶级目录
906 | # 将会返回以下
907 | # media_ass: 媒体文件与字幕文件的对应词典
908 | # 结构: { 媒体文件绝对路径 : [ 字幕1绝对路径, 字幕2绝对路径, ...] }
909 | def getSubtitles(medias: list, cpath: str) -> dict:
910 | media_ass = {}
911 | global s_subdir, matchStrict
912 | if s_subdir:
913 | for r,ds,fs in os.walk(cpath):
914 | for f in [path.join(r, s) for s in fs if path.splitext(s)[1][1:].lower() in subext]:
915 | if '.subset' in path.basename(f): continue
916 | for l in medias:
917 | vdir = path.dirname(l[1])
918 | sdir = path.dirname(f)
919 | sext = path.splitext(f)
920 | if (l[0] in f and not matchStrict) or (l[0] == path.basename(f)[:len(l[0])] and matchStrict):
921 | if((vdir in sdir and sdir not in vdir) or (vdir == sdir)):
922 | if sext[1][:1].lower() == 'idx':
923 | if not path.exists(sext[1] + '.sub'):
924 | continue
925 | if media_ass.get(l[1]) is None:
926 | media_ass[l[1]] = [f]
927 | else: media_ass[l[1]].append(f)
928 | else:
929 | for f in [path.join(cpath, s) for s in os.listdir(cpath) if not path.isdir(s) and
930 | path.splitext(s)[1][1:].lower() in subext]:
931 | # print(f, cpath)
932 | if '.subset' in path.basename(f): continue
933 | for l in medias:
934 | # print(path.basename(f)[len(l[0]):], l)
935 | sext = path.splitext(f)
936 | if (l[0] in f and not matchStrict) or (l[0] == path.basename(f)[:len(l[0])] and matchStrict):
937 | if path.dirname(l[1]) == path.dirname(f):
938 | if sext[1][:1].lower() == 'idx':
939 | if not path.exists(sext[1] + '.sub'):
940 | continue
941 | if media_ass.get(l[1]) is None:
942 | media_ass[l[1]] = [f]
943 | else: media_ass[l[1]].append(f)
944 | return media_ass
945 |
946 | # 主函数,负责调用各函数走完完整的处理流程
947 | # 需要以下输入
948 | # font_name: 字体名称与字体路径对应词典,结构见 fontProgress
949 | # asspath: 字幕绝对路径列表
950 | # 以下输入可选
951 | # outdir: 输出目录,格式 [ 字幕输出目录, 字体输出目录, 视频输出目录 ],如果项数不足,则取最后一项;默认为 asspaths 中每项所在目录
952 | # mux: 不要封装,只运行到子集化完成
953 | # vpath: 视频路径,只在 mux = True 时生效
954 | # asslangs: 字幕语言列表,将会按照顺序赋给对应的字幕轨道,只在 mux = True 时生效
955 | # 将会返回以下
956 | # newasspath: 列表,新生成的字幕文件绝对路径
957 | # newfont_name: 词典,{ 原字体名 : [ 新字体绝对路径, 新字体名 ] }
958 | # ??? : 数值,mkvmerge的返回值;如果 mux = False,返回-1
959 | def main(font_name: dict, asspath: list, outdir: list = ['', '', ''], mux: bool = False, vpath: str = '', asslangs: list = []):
960 | print('')
961 | outdir_temp = outdir[:3]
962 | outdir = ['', '', '']
963 | for i in range(0, len(outdir_temp)):
964 | s = outdir_temp[i]
965 | # print(s)
966 | if s is None:
967 | outdir[i] = ''
968 | elif s == '':
969 | outdir[i] = s
970 | else:
971 | try:
972 | if not path.isdir(s) and path.exists(path.dirname(s)):
973 | os.mkdir(s)
974 | if path.isdir(s): outdir[i] = s
975 | except:
976 | print('\033[1;31m[ERROR] 创建输出文件夹错误\n[ERROR] {0}\033[0m'.format(sys.exc_info()))
977 | if '\\' in s:
978 | outdir[i] = path.join(os.getcwd(), path.basename(s.rstrip('\\')))
979 | else: outdir[i] = path.join(os.getcwd(), s)
980 | # print(outdir)
981 | # os.system('pause')
982 | global notfont
983 | # multiAss 多ASS文件输入记录词典
984 | # 结构: { ASS文件绝对路径 : [ 完整ASS文件内容(fullass), 样式位置(styleline), 字体在样式行中的位置(font_pos) ]}
985 | multiAss = {}
986 | assfont = {}
987 | fontlist = {}
988 | newasspath = []
989 | fo = ''
990 | if not notfont:
991 | # print('\n字体名称总数: {0}'.format(len(font_name.keys())))
992 | # noass = False
993 | for i in range(0, len(asspath)):
994 | s = asspath[i]
995 | if path.splitext(s)[1][1:].lower() not in ['ass', 'ssa']:
996 | multiAss[s] = [[], 0, 0]
997 | else:
998 | # print('正在分析字幕文件: \"{0}\"'.format(path.basename(s)))
999 | fullass, fontlist, styleline, font_pos, fn_lines = assAnalyze(s, fontlist)
1000 | multiAss[s] = [fullass, styleline, font_pos]
1001 | assfont, font_name = checkAssFont(fontlist, font_name, assfont)
1002 | if assfont is None:
1003 | return None, None, -2
1004 | sn = path.splitext(path.basename(asspath[0]))[0]
1005 | fn = path.join(path.dirname(asspath[0]), 'Fonts')
1006 | if outdir[1] == '': outdir[1] = fn
1007 | if not path.isdir(outdir[1]):
1008 | try:
1009 | os.mkdir(outdir[1])
1010 | fo = path.join(outdir[1], sn)
1011 | except:
1012 | fo = path.join(path.dirname(outdir[1]), sn)
1013 | else:
1014 | fo = path.join(outdir[1], sn)
1015 | newfont_name = assFontSubset(assfont, fo)
1016 | for s in asspath:
1017 | if path.splitext(s)[1][1:].lower() not in ['ass', 'ssa']:
1018 | newasspath.append(s)
1019 | elif len(multiAss[s][0]) == 0 or multiAss[s][1] == multiAss[s][2]:
1020 | continue
1021 | else: newasspath.append(assFontChange(multiAss[s][0], newfont_name, s, multiAss[s][1], multiAss[s][2], outdir[0], fn_lines=fn_lines))
1022 | else:
1023 | newasspath = asspath
1024 | newfont_name = {}
1025 | if mux:
1026 | if outdir[2] == '': outdir[2] = path.dirname(vpath)
1027 | if not path.isdir(outdir[2]):
1028 | try:
1029 | os.mkdir(outdir[2])
1030 | except:
1031 | outdir[2] = path.dirname(outdir[2])
1032 | mkvr = ASFMKV(vpath, path.join(outdir[2], path.splitext(path.basename(vpath))[0] + '.mkv'),
1033 | asslangs=asslangs, asspaths=newasspath, fontpaths=list(newfont_name.values()))
1034 | if not notfont:
1035 | for ap in newasspath:
1036 | if path.exists(path.dirname(ap)) and path.splitext(ap)[1][1:].lower() not in ['ass', 'ssa']:
1037 | try:
1038 | os.rmdir(path.dirname(ap))
1039 | except:
1040 | break
1041 | for fp in newfont_name.keys():
1042 | if path.exists(path.dirname(newfont_name[fp][0])):
1043 | try:
1044 | os.rmdir(path.dirname(newfont_name[fp][0]))
1045 | except:
1046 | continue
1047 | if path.isdir(fo):
1048 | try:
1049 | os.rmdir(fo)
1050 | except:
1051 | pass
1052 | return newasspath, newfont_name, mkvr
1053 | else:
1054 | return newasspath, newfont_name, -1
1055 |
1056 | def cls():
1057 | os.system('cls')
1058 |
1059 |
1060 | # 初始化字体列表 和 mkvmerge 相关参数
1061 | os.system('title ASFMKV Python Remake 1.01 ^| (c) 2022 yyfll ^| Apache-2.0')
1062 | fontlist, font_name = getFileList(fontin)
1063 | font_name = fontProgress(fontlist, font_name)
1064 | no_mkvm = False
1065 | no_cmdc = False
1066 | mkvmv = '\n\033[1;33m没有检测到 mkvmerge\033[0m'
1067 | if os.system('mkvmerge -V 1>nul 2>nul') > 0:
1068 | no_mkvm = True
1069 | else:
1070 | print('\n\n\033[1;33m正在获取 mkvmerge 语言编码列表和支持格式列表,请稍等...\033[0m')
1071 | mkvmv = '\n' + os.popen('mkvmerge -V --ui-language en', mode='r').read().replace('\n', '')
1072 | extget = re.compile(r'\[.*\]')
1073 | langmkv = os.popen('mkvmerge --list-languages', mode='r')
1074 | for s in langmkv.buffer.read().decode('utf-8').splitlines()[2:]:
1075 | s = s.replace('\n', '').split('|')
1076 | for si in range(1, len(s)):
1077 | ss = s[si]
1078 | if len(ss.strip(' ')) > 0:
1079 | langlist.append(ss.strip(' '))
1080 | langmkv.close()
1081 | for s in os.popen('mkvmerge -l --ui-language en', mode='r').readlines()[1:]:
1082 | for ss in re.search(r'\[.*\]', s).group().lstrip('[').rstrip(']').split(' '):
1083 | extsupp.append(ss)
1084 | extl_c = extlist
1085 | extlist = []
1086 | print('')
1087 | for i in range(0, len(extl_c)):
1088 | s = extl_c[i]
1089 | if s in extsupp:
1090 | extlist.append(s)
1091 | else:
1092 | print('\033[1;31m[WARNING] 您设定的媒体扩展名 {0} 无效,已从列表移除\033[0m'.format(s))
1093 | if len(extlist) != len(extl_c):
1094 | print('\n\033[1;33m当前的媒体扩展名列表: \"{0}\"\033[0m\n'.format(';'.join(extlist)))
1095 | os.system('pause')
1096 | del extl_c
1097 | if len(sublang) > 0:
1098 | print('')
1099 | sublang_c = sublang
1100 | sublang = []
1101 | for i in range(0, len(sublang_c)):
1102 | s = sublang_c[i]
1103 | if s in langlist:
1104 | sublang.append(s)
1105 | else:
1106 | print('\033[1;31m[WARNING] 您设定的语言编码 {0} 无效,已从列表移除\033[0m'.format(s))
1107 | if len(sublang) != len(sublang_c):
1108 | print('\n\033[1;33m当前的语言编码列表: \"{0}\"\033[0m\n'.format(';'.join(sublang)))
1109 | os.system('pause')
1110 | del sublang_c
1111 |
1112 |
1113 | def cListAssFont(font_name):
1114 | global resultw, s_subdir, copyfont
1115 | leave = True
1116 | while leave:
1117 | cls()
1118 | print('''ASFMKV-ListAssFontPy
1119 | 注意: 本程序由于设计原因,列出的是字体文件与其内部字体名的对照表
1120 |
1121 | 选择功能:
1122 | [L] 回到上级菜单
1123 | [A] 列出全部字体
1124 | [B] 检查并列出字幕所需字体
1125 |
1126 | 切换开关:
1127 | [1] 将结果写入文件: \033[1;33m{0}\033[0m
1128 | [2] 拷贝所需字体: \033[1;33m{1}\033[0m
1129 | [3] 搜索子目录(字幕): \033[1;33m{2}\033[0m
1130 | '''.format(resultw, copyfont, s_subdir))
1131 | work = os.system('choice /M 请输入 /C AB123L')
1132 | if work == 1:
1133 | cls()
1134 | wfilep = path.join(os.getcwd(), datetime.now().strftime('ASFMKV_FullFont_%Y-%m%d-%H%M-%S_%f.log'))
1135 | if resultw:
1136 | wfile = open(wfilep, mode='w', encoding='utf-8-sig')
1137 | else: wfile = None
1138 | fn = ''
1139 | print('FontList', file=wfile)
1140 | for s in font_name.keys():
1141 | nfn = path.basename(font_name[s][0])
1142 | if fn != nfn:
1143 | if wfile is not None:
1144 | print('>\n{0} <{1}'.format(nfn, s), end='', file=wfile)
1145 | else: print('>\033[0m\n\033[1;36m{0}\033[0m \033[1m<{1}'.format(nfn, s), end='')
1146 | else:
1147 | print(', {0}'.format(s), end='', file=wfile)
1148 | fn = nfn
1149 | if wfile is not None:
1150 | print(wfilep)
1151 | print('>', file=wfile)
1152 | wfile.close()
1153 | else: print('>\033[0m')
1154 | elif work == 2:
1155 | cls()
1156 | cpath = ''
1157 | directout = False
1158 | while not path.exists(cpath) and not directout:
1159 | directout = True
1160 | cpath = input('请输入目录路径或字幕文件路径: ').strip('\"')
1161 | if cpath == '' : print('没有输入,回到上级菜单')
1162 | elif not path.exists(cpath): print('\033[1;31m[ERROR] 找不到路径: \"{0}\"\033[0m'.format(cpath))
1163 | elif not path.isfile(cpath) and not path.isdir(cpath): print('\033[1;31m[ERROR] 输入的必须是文件或目录!: \"{0}\"\033[0m'.format(cpath))
1164 | elif not path.isabs(cpath): print('\033[1;31m[ERROR] 路径必须为绝对路径!: \"{0}\"\033[0m'.format(cpath))
1165 | elif path.isdir(cpath): directout = False
1166 | elif not path.splitext(cpath)[1][1:].lower() in ['ass', 'ssa']:
1167 | print('\033[1;31m[ERROR] 输入的必须是ASS/SSA字幕文件!: \"{0}\"\033[0m'.format(cpath))
1168 | else: directout = False
1169 | #clist = []
1170 | fontlist = {}
1171 | assfont = {}
1172 | if not directout:
1173 | if path.isdir(cpath):
1174 | #print(cpath)
1175 | dir = ''
1176 | if s_subdir:
1177 | for r,ds,fs in os.walk(cpath):
1178 | for f in fs:
1179 | if path.splitext(f)[1][1:].lower() in ['ass', 'ssa']:
1180 | a, fontlist, b, c, d = assAnalyze(path.join(r, f), fontlist, onlycheck=True)
1181 | assfont, font_name = checkAssFont(fontlist, font_name, assfont, onlycheck=True)
1182 | else:
1183 | for f in os.listdir(cpath):
1184 | if path.isfile(path.join(cpath, f)):
1185 | #print(f, path.splitext(f)[1][1:].lower())
1186 | if path.splitext(f)[1][1:].lower() in ['ass', 'ssa']:
1187 | #print(f, 'pass')
1188 | a, fontlist, b, c, d = assAnalyze(path.join(cpath, f), fontlist, onlycheck=True)
1189 | assfont, font_name = checkAssFont(fontlist, font_name, assfont, onlycheck=True)
1190 | fd = path.join(cpath, 'Fonts')
1191 | else:
1192 | a, fontlist, b, c, d = assAnalyze(cpath, fontlist, onlycheck=True)
1193 | assfont, font_name = checkAssFont(fontlist, font_name, assfont, onlycheck=True)
1194 | fd = path.join(path.dirname(cpath), 'Fonts')
1195 | if len(assfont.keys()) < 1:
1196 | print('\033[1;31m[ERROR] 目标路径没有ASS/SSA字幕文件\033[0m')
1197 | else:
1198 | wfile = None
1199 | print('')
1200 | if copyfont or resultw:
1201 | if not path.isdir(fd): os.mkdir(fd)
1202 | if resultw:
1203 | wfile = open(path.join(cpath, 'Fonts', 'fonts.txt'), mode='w', encoding='utf-8-sig')
1204 | maxnum = 0
1205 | for s in assfont.keys():
1206 | ssp = s.split('?')
1207 | if not ssp[0] == ssp[1]:
1208 | lx = len(assfont[s][0])
1209 | if lx > maxnum:
1210 | maxnum = lx
1211 | maxnum = len(str(maxnum))
1212 | for s in assfont.keys():
1213 | ssp = s.split('?')
1214 | if not ssp[0] == ssp[1]:
1215 | fp = ssp[0]
1216 | fn = path.basename(fp)
1217 | ann = ''
1218 | errshow = False
1219 | if copyfont:
1220 | try:
1221 | shutil.copy(fp, path.join(fd, fn))
1222 | ann = ' - copied'
1223 | except:
1224 | print('[ERROR]', sys.exc_info())
1225 | ann = ' - copy error'
1226 | errshow = True
1227 | if resultw:
1228 | print('{0} <{1}>{2}'.format(assfont[s][1], path.basename(fn), ann), file=wfile)
1229 | if errshow:
1230 | print('\033[1;32m[{3}]\033[0m \033[1;36m{0}\033[0m \033[1m<{1}>\033[1;31m{2}\033[0m'.format(assfont[s][1],
1231 | path.basename(fn), ann, str(len(assfont[s][0])).rjust(maxnum)))
1232 | else: print('\033[1;32m[{3}]\033[0m \033[1;36m{0}\033[0m \033[1m<{1}>\033[1;32m{2}\033[0m'.format(assfont[s][1],
1233 | path.basename(fn), ann, str(len(assfont[s][0])).rjust(maxnum)))
1234 | # print(assfont[s][0])
1235 | else:
1236 | if resultw:
1237 | print('{0} - No Found'.format(ssp[0]), file=wfile)
1238 | print('\033[1;31m[{1}]\033[1;36m {0}\033[1;31m - No Found\033[0m'.format(ssp[0], 'N'.center(maxnum, 'N')))
1239 | if resultw: wfile.close()
1240 | print('')
1241 | del assfont
1242 | del fontlist
1243 | elif work == 3:
1244 | if resultw: resultw = False
1245 | else: resultw = True
1246 | elif work == 4:
1247 | if copyfont: copyfont = False
1248 | else: copyfont = True
1249 | elif work == 5:
1250 | if s_subdir: s_subdir = False
1251 | else: s_subdir = True
1252 | else:
1253 | leave = False
1254 | if work < 3: os.system('pause')
1255 |
1256 |
1257 | def checkOutPath(op: str, default: str) -> str:
1258 | if op == '': return default
1259 | if op[0] == '?': return '?' + re.sub(cillegal, '_', op[1:])
1260 | if not path.isabs(op):
1261 | print('\033[1;31m[ERROR] 输入的必须是绝对路径或子目录名称!: \"{0}\"\033[0m'.format(op))
1262 | os.system('pause')
1263 | return default
1264 | if path.isdir(op): return op
1265 | if path.isfile(op): return path.dirname(op)
1266 | print('\033[1;31m[ERROR] 输入的必须是目录路径或子目录名称!: \"{0}\"\033[0m'.format(op))
1267 | os.system('pause')
1268 | return default
1269 |
1270 | def showMessageSubset(newasspaths: list, newfont_name: dict):
1271 | for ap in newasspaths:
1272 | if path.exists(ap):
1273 | print('\033[1;32m成功:\033[0m \033[1m\"{0}\"\033[0m'.format(path.basename(ap)))
1274 | for nf in newfont_name.keys():
1275 | if path.exists(newfont_name[nf][0]):
1276 | if newfont_name[nf][1] is None:
1277 | print('\033[1;31m失败:\033[1m \"{0}\"\033[0m >> \033[1m\"{1}\"\033[0m'.format(path.basename(nf),
1278 | path.basename(newfont_name[nf][0])))
1279 | else:
1280 | print('\033[1;32m成功:\033[1m \"{0}\"\033[0m >> \033[1m\"{1}\" ({2})\033[0m'.format(path.basename(nf),
1281 | path.basename(newfont_name[nf][0]), newfont_name[nf][1]))
1282 |
1283 | def cFontSubset(font_name):
1284 | global extlist, v_subdir, s_subdir, rmAssIn, rmAttach, \
1285 | mkvout, assout, fontout, matchStrict, no_mkvm, notfont
1286 | leave = True
1287 | while leave:
1288 | cls()
1289 | print('''ASFMKV & ASFMKV-FontSubset
1290 |
1291 | 选择功能:
1292 | [L] 回到上级菜单
1293 | [A] 子集化字体
1294 | [B] 子集化并封装
1295 |
1296 | 切换开关:
1297 | [1] 检视媒体扩展名列表 及 语言编码列表
1298 | [2] 搜索子目录(视频): \033[1;33m{0}\033[0m
1299 | [3] 搜索子目录(字幕): \033[1;33m{1}\033[0m
1300 | [4] (封装)移除内挂字幕: \033[1;33m{2}\033[0m
1301 | [5] (封装)移除原有附件: \033[1;33m{3}\033[0m
1302 | [6] (封装)不封装字体: \033[1;33m{8}\033[0m
1303 | [7] 严格字幕匹配: \033[1;33m{7}\033[0m
1304 | [8] 媒体文件输出文件夹: \033[1;33m{4}\033[0m
1305 | [9] 字幕文件输出文件夹: \033[1;33m{5}\033[0m
1306 | [0] 字体文件输出文件夹: \033[1;33m{6}\033[0m
1307 | '''.format(v_subdir, s_subdir, rmAssIn, rmAttach, mkvout, assout,
1308 | fontout, matchStrict, notfont))
1309 | work = 0
1310 | work = os.system('choice /M 请输入 /C AB1234567890L')
1311 | if work == 2 and no_mkvm:
1312 | print('[ERROR] 在您的系统中找不到 mkvmerge, 该功能不可用')
1313 | work = -1
1314 | if work in [1, 2]:
1315 | cls()
1316 | if work == 1: print('''子集化字体
1317 |
1318 | 搜索子目录(视频): \033[1;33m{0}\033[0m
1319 | 搜索子目录(字幕): \033[1;33m{1}\033[0m
1320 | 严格字幕匹配: \033[1;33m{2}\033[0m
1321 | 字幕文件输出文件夹: \033[1;33m{3}\033[0m
1322 | 字体文件输出文件夹: \033[1;33m{4}\033[0m
1323 | '''.format(v_subdir, s_subdir, matchStrict, assout, fontout))
1324 | else: print('''子集化字体并封装
1325 |
1326 | 搜索子目录(视频): \033[1;33m{4}\033[0m
1327 | 搜索子目录(字幕): \033[1;33m{5}\033[0m
1328 | 移除内挂字幕: \033[1;33m{0}\033[0m
1329 | 移除原有附件: \033[1;33m{1}\033[0m
1330 | 不封装字体: \033[1;33m{2}\033[0m
1331 | 严格字幕匹配: \033[1;33m{6}\033[0m
1332 | 媒体文件输出文件夹: \033[1;33m{3}\033[0m
1333 | '''.format(rmAssIn, rmAttach, notfont, mkvout, v_subdir, s_subdir, matchStrict))
1334 | cpath = ''
1335 | directout = False
1336 | subonly = False
1337 | subonlyp = ''
1338 | while not path.exists(cpath) and not directout:
1339 | directout = True
1340 | cpath = input('不输入任何值 直接回车回到上一页面\n请输入文件或目录路径: ').strip('\"')
1341 | if cpath == '': print('没有输入,回到上级菜单')
1342 | elif not path.isabs(cpath): print('\033[1;31m[ERROR] 输入的必须是绝对路径!: \"{0}\"\033[0m'.format(cpath))
1343 | elif not path.exists(cpath): print('\033[1;31m[ERROR] 找不到路径: \"{0}\"\033[0m'.format(cpath))
1344 | elif path.isfile(cpath):
1345 | testext = path.splitext(cpath)[1][1:].lower()
1346 | if testext in extlist:
1347 | directout = False
1348 | elif testext in ['ass', 'ssa'] and work == 1:
1349 | directout = False
1350 | subonly = True
1351 | else: print('\033[1;31m[ERROR] 扩展名不正确: \"{0}\"\033[0m'.format(cpath))
1352 | elif not path.isdir(cpath):
1353 | print('\033[1;31m[ERROR] 输入的应该是目录或媒体文件!: \"{0}\"\033[0m'.format(cpath))
1354 | else: directout = False
1355 | # print(directout)
1356 | if not directout:
1357 | medias = None
1358 | if path.isfile(cpath):
1359 | if not subonly: medias = [[path.splitext(path.basename(cpath))[0], cpath]]
1360 | else: subonlyp = cpath
1361 | cpath = path.dirname(cpath)
1362 | else: medias = getMediaFilelist(cpath)
1363 | # print(medias)
1364 |
1365 | if assout == '':
1366 | assout_cache = path.join(cpath, 'Subtitles')
1367 | elif assout[0] == '?':
1368 | assout_cache = path.join(cpath, assout[1:])
1369 | else: assout_cache = assout
1370 | if fontout == '':
1371 | fontout_cache = path.join(cpath, 'Fonts')
1372 | elif fontout[0] == '?':
1373 | fontout_cache = path.join(cpath, fontout[1:])
1374 | else: fontout_cache = fontout
1375 | if mkvout == '':
1376 | mkvout_cache = ''
1377 | elif mkvout[0] == '?':
1378 | mkvout_cache = path.join(cpath, mkvout[1:])
1379 | else: mkvout_cache = mkvout
1380 |
1381 | domux = False
1382 | if work == 2: domux = True
1383 |
1384 | if not medias is None:
1385 | if len(medias) > 0:
1386 | media_ass = getSubtitles(medias, cpath)
1387 | # print(media_ass)
1388 | for k in media_ass.keys():
1389 | #print(k)
1390 | #print([assout_cache, fontout_cache, mkvout_cache])
1391 | newasspaths, newfont_name, mkvr = main(font_name, media_ass[k],
1392 | mux = domux, outdir=[assout_cache, fontout_cache, mkvout_cache], vpath=k, asslangs=sublang)
1393 | if mkvr != -2:
1394 | showMessageSubset(newasspaths, newfont_name)
1395 | else:
1396 | break
1397 | elif subonly:
1398 | newasspaths, newfont_name, mkvr = main(font_name, [subonlyp],
1399 | mux = domux, outdir=[assout_cache, fontout_cache, mkvout_cache])
1400 | if mkvr != -2:
1401 | showMessageSubset(newasspaths, newfont_name)
1402 | elif work == 3:
1403 | cls()
1404 | print('ExtList Viewer 1.00-Final\n')
1405 | for i in range(0, len(extlist)):
1406 | s = extlist[i]
1407 | print('[Ext{0:>3d}] {1}'.format(i, s))
1408 | print('\n')
1409 | if not no_mkvm:
1410 | os.system('pause')
1411 | cls()
1412 | os.system('mkvmerge --list-languages')
1413 | #print('\033[1m mkvmerge 语言编码列表 \033[0m')
1414 | else:
1415 | print('没有检测到mkvmerge,无法输出语言编码列表')
1416 | elif work == 4:
1417 | if v_subdir: v_subdir = False
1418 | else: v_subdir = True
1419 | elif work == 5:
1420 | if s_subdir: s_subdir = False
1421 | else: s_subdir = True
1422 | elif work == 6:
1423 | if rmAssIn: rmAssIn = False
1424 | else: rmAssIn = True
1425 | elif work == 7:
1426 | if rmAttach: rmAttach = False
1427 | else: rmAttach = True
1428 | elif work == 8:
1429 | if notfont: notfont = False
1430 | else: notfont = True
1431 | elif work == 9:
1432 | if matchStrict: matchStrict = False
1433 | else: matchStrict = True
1434 | elif work in [10, 11, 12]:
1435 | cls()
1436 | print('''请输入目标目录路径或子目录名称\n(若要输入子目录,请在目录名前加\"?\")
1437 | (不支持多层子目录,会自动将\"\\\"换成下划线)''')
1438 | if work == 10:
1439 | mkvout = checkOutPath(input(), mkvout)
1440 | elif work == 11:
1441 | assout = checkOutPath(input(), assout)
1442 | elif work == 12:
1443 | fontout = checkOutPath(input(), fontout)
1444 | else:
1445 | leave = False
1446 | if work < 4:
1447 | os.system('pause')
1448 |
1449 | def cLicense():
1450 | cls()
1451 | print('''AddSubFontMKV Python Remake 1.01
1452 | Apache-2.0 License
1453 | Copyright(c) 2022 yyfll
1454 |
1455 | 依赖:
1456 | fontTools | MIT License
1457 | chardet | LGPL-2.1 License
1458 | colorama | BSD-3 License
1459 | mkvmerge | GPL-2 License
1460 | ''')
1461 | print('for more information:\nhttps://www.apache.org/licenses/')
1462 | os.system('pause')
1463 |
1464 | cls()
1465 |
1466 | if os.system('choice /? 1>nul 2>nul') > 0:
1467 | no_cmdc = True
1468 |
1469 | if __name__=="__main__":
1470 | while 1:
1471 | work = 0
1472 | print('''ASFMKV Python Remake 1.01 | (c) 2022 yyfll{0}
1473 | 字体名称数: [\033[1;33m{2}\033[0m](包含乱码的名称)
1474 |
1475 | 请选择功能:
1476 | [A] ListAssFont
1477 | [B] 字体子集化 & MKV封装
1478 | [C] 检视重复字体: 重复名称[\033[1;33m{1}\033[0m]
1479 |
1480 | 其他:
1481 | [D] 依赖与许可证
1482 | [L] 直接退出'''.format(mkvmv, len(dupfont.keys()), len(font_name.keys())))
1483 | print('')
1484 | work = os.system('choice /M 请选择: /C ABCDL')
1485 | if work == 1:
1486 | cListAssFont(font_name)
1487 | elif work == 2:
1488 | cFontSubset(font_name)
1489 | elif work == 3:
1490 | cls()
1491 | if len(dupfont.keys()) > 0:
1492 | for s in dupfont.keys():
1493 | print('\033[1;31m[{0}]\033[0m'.format(s))
1494 | for ss in dupfont[s]:
1495 | print('\"{0}\"'.format(ss))
1496 | print('')
1497 | else:
1498 | print('没有名称重复的字体')
1499 | os.system('pause')
1500 | elif work == 4:
1501 | cLicense()
1502 | elif work == 5:
1503 | exit()
1504 | else: exit()
1505 | cls()
--------------------------------------------------------------------------------
/ASFMKV_py1.02-pre.py:
--------------------------------------------------------------------------------
1 | # -*- coding: UTF-8 -*-
2 | # *************************************************************************
3 | #
4 | # 请使用支持 UTF-8 NoBOM 并最好带有 Python 语法高亮的文本编辑器
5 | # Windows 7 的用户请最好不要使用 写字板/记事本 打开本脚本
6 | #
7 | # *************************************************************************
8 |
9 | # 调用库,请不要修改
10 | import shutil
11 | from fontTools import ttLib
12 | from fontTools import subset
13 | from chardet.universaldetector import UniversalDetector
14 | import os
15 | from os import path
16 | import sys
17 | import re
18 | import winreg
19 | import zlib
20 | import json
21 | from colorama import init
22 | from datetime import datetime
23 |
24 | # 初始化环境变量
25 | # *************************************************************************
26 | # 自定义变量
27 | # 修改注意: Python的布尔类型首字母要大写 True 或 False,在名称中有单引号的,需要输入反斜杠转义 \'
28 | # *************************************************************************
29 | # extlist 可输入的视频媒体文件扩展名
30 | extlist = 'mkv;mp4;mts;mpg;flv;mpeg;m2ts;avi;webm;rm;rmvb;mov;mk3d;vob'
31 | # *************************************************************************
32 | # no_extcheck 关闭对extlist的扩展名检查来添加一些可能支持的格式
33 | no_extcheck = False
34 | # *************************************************************************
35 | # mkvout 媒体文件输出目录(封装)
36 | # 在最前方用"?"标记来表示这是一个子目录
37 | # 注意: 在Python中需要在左侧引号前加 r 来保留 Windows 路径中的反斜杠,路径末尾不需要反斜杠
38 | mkvout = ''
39 | # *************************************************************************
40 | # assout 字幕文件输出目录
41 | # 在最前方用"?"标记来表示这是一个子目录
42 | # 注意: 在Python中需要在左侧引号前加 r 来保留 Windows 路径中的反斜杠,路径末尾不需要反斜杠
43 | assout = '?subs'
44 | # *************************************************************************
45 | # fontout 字体文件输出目录
46 | # 在最前方用"?"标记来表示这是一个子目录
47 | # 注意: 在Python中需要在左侧引号前加 r 来保留 Windows 路径中的反斜杠,路径末尾不需要反斜杠
48 | fontout = '?Fonts'
49 | # *************************************************************************
50 | # fontin 自定义字体文件夹,可做额外字体源,必须是绝对路径
51 | # 可以有多个路径,路径之间用"?"来分隔
52 | # 注意: 在Python中需要在左侧引号前加 r 来保留 Windows 路径中的反斜杠,路径末尾不需要反斜杠
53 | fontin = r''
54 | # *************************************************************************
55 | # notfont (封装)字体嵌入
56 | # True 始终嵌入字体
57 | # False 不嵌入字体,不子集化字体,不替换字幕中的字体信息
58 | notfont = False
59 | # *************************************************************************
60 | # sublang (封装)字幕语言
61 | # 会按照您所输入的顺序给字幕赋予语言编码,如果字幕数多于语言数,多出部分将赋予最后一种语言编码
62 | # IDX+SUB 的 DVDSUB 由于 IDX 文件一般有语言信息,对于DVDSUB不再添加语言编码
63 | # 各语言之间应使用半角分号 ; 隔开,如 'chi;chi;chi;und'
64 | # 可以在 mkvmerge -l 了解语言编码
65 | sublang = ''
66 | # *************************************************************************
67 | # matchStrict 严格匹配
68 | # True 媒体文件名必须在字幕文件名的最前方,如'test.mkv'的字幕可以是'test.ass'或是'test.sub.ass',但不能是'sub.test.ass'
69 | # False 只要字幕文件名中有媒体文件名就行了,不管它在哪
70 | matchStrict = True
71 | # *************************************************************************
72 | # rmAssIn (封装)如果输入文件是mkv,删除mkv文件中原有的字幕
73 | rmAssIn = True
74 | # *************************************************************************
75 | # rmAttach (封装)如果输入文件是mkv,删除mkv文件中原有的附件
76 | rmAttach = True
77 | # *************************************************************************
78 | # v_subdir 视频的子目录搜索
79 | v_subdir = False
80 | # *************************************************************************
81 | # s_subdir 字幕的子目录搜索
82 | s_subdir = False
83 | # *************************************************************************
84 | # copyfont (ListAssFont)拷贝字体到源文件夹
85 | copyfont = False
86 | # *************************************************************************
87 | # resultw (ListAssFont)打印结果到源文件夹
88 | resultw = False
89 | # *************************************************************************
90 |
91 | # 以下变量谨慎更改
92 | # subext 可输入的字幕扩展名,按照python列表语法
93 | subext = ['ass', 'ssa', 'srt', 'sup', 'idx']
94 |
95 |
96 | # 以下环境变量不应更改
97 | # 编译 style行 搜索用正则表达式
98 | style_read = re.compile('.*\nStyle:.*')
99 | cillegal = re.compile(r'[\\/:\*"><\|]')
100 | # 切分extlist列表
101 | extlist = [s.strip(' ').lstrip('.').lower() for s in extlist.split(';') if len(s) > 0]
102 | # 切分sublang列表
103 | sublang = [s.strip(' ').lower() for s in sublang.split(';') if len(s) > 0]
104 | # 切分fontin列表
105 | fontin = [s.strip(' ') for s in fontin.split('?') if len(s) > 0]
106 | fontin = [s for s in fontin if path.isdir(s)]
107 | langlist = []
108 | extsupp = []
109 | dupfont = {}
110 | font_n_lower = {}
111 | font_family = {}
112 | init()
113 |
114 | def fontlistAdd(s: str, fn: str, fontlist: dict) -> dict:
115 | if len(s) > 0:
116 | si = 0
117 | fn = fn.lstrip('@')
118 | # print(fn, s)
119 | if fontlist.get(fn) is None:
120 | fontlist[fn] = s[0]
121 | si += 1
122 | for i in range(si, len(s)):
123 | if not s[i] in fontlist[fn]:
124 | fontlist[fn] = fontlist[fn] + s[i]
125 | return fontlist
126 |
127 | # ASS分析部分
128 | # 需要输入
129 | # asspath: ASS文件的绝对路径
130 | # font_name: 字体名称与字体绝对路径词典,用于查询 Bold、Italic 字体
131 | # 可选输入
132 | # fontlist: 可以更新fontlist,用于多ASS同时输入的情况,结构见下
133 | # onlycheck: 只确认字幕中的字体,仅仅返回fontlist
134 | # 将会返回
135 | # fullass: 完整的ASS文件内容,以行分割
136 | # fontlist: 字体与其所需字符 { 字体 : 字符串 }
137 | # styleline: 样式内容的起始行
138 | # font_pos: 字体在样式中的位置
139 | # fn_lines: 带有fn标签的行数与该行的完整特效标签,一项一个 [ [行数, 标签1, 标签2], ... ]
140 | def assAnalyze(asspath: str, fontlist: dict = {}, onlycheck: bool = False):
141 | global style_read
142 | # 初始化变量
143 | eventline = 0
144 | style_pos = 0
145 | style_pos2 = 0
146 | text_pos = 0
147 | font_pos = 0
148 | bold_pos = 0
149 | italic_pos = 0
150 | styleline = 0
151 | fn_lines = []
152 | # 编译分析用正则表达式
153 | event_read = re.compile('.*\nDialogue:.*')
154 | style = re.compile(r'^\[V4.*Styles\]$')
155 | event = re.compile(r'^\[Events\]$')
156 | # 识别文本编码并读取整个SubtitleStationAlpha文件到内存
157 | print('\033[1;33m正在分析字幕: \033[1;37m\"{0}\"\033[0m'.format(path.basename(asspath)))
158 | ass = open(asspath, mode='rb')
159 | # if path.getsize(asspath) <= 100 * 1024:
160 | # ass_b = ass.read()
161 | # ass_code = chardet.detect(ass_b)['encoding'].lower()
162 | # else:
163 | detector = UniversalDetector()
164 | for dt in ass:
165 | detector.feed(dt)
166 | if detector.done:
167 | ass_code = detector.result['encoding']
168 | break
169 | detector.reset()
170 | ass.close()
171 | ass = open(asspath, encoding=ass_code, mode='r')
172 | fullass = ass.readlines()
173 | ass.close()
174 | asslen = len(fullass)
175 | # 在文件中搜索Styles标签和Events标签来确认起始行
176 | for s in range(0, asslen):
177 | if re.match(style, fullass[s]) is not None:
178 | styleline = s
179 | elif re.match(event, fullass[s]) is not None:
180 | eventline = s
181 | if styleline != 0 and eventline != 0:
182 | break
183 |
184 | # 获取Style的 Format 行,并用半角逗号分割
185 | style_format = ''.join(fullass[styleline + 1].split(':')[1:]).strip(' ').split(',')
186 | # 确定Style中 Name 和 Fontname 的位置
187 | for i in range(0, len(style_format)):
188 | s = style_format[i].lower().strip(' ').replace('\n', '')
189 | if s == 'name':
190 | style_pos = i
191 | elif s == 'fontname':
192 | font_pos = i
193 | elif s == 'bold':
194 | bold_pos = i
195 | elif s == 'italic':
196 | italic_pos = i
197 | if style_pos != 0 and font_pos != 0 and bold_pos != 0 and italic_pos != 0:
198 | break
199 |
200 | # 获取 字体表 与 样式字体对应表
201 | style_font = {}
202 | # style_font 词典内容:
203 | # { 样式 : 字体名 }
204 | # fontlist 词典内容:
205 | # { 字体名?斜体?粗体 : 使用该字体的文本 }
206 | for i in range(styleline + 2, asslen):
207 | if len(fullass[i].split(':')) < 2:
208 | if i + 1 > asslen:
209 | break
210 | else:
211 | if asslen - (i + 1) > 30:
212 | searchr = i + 31
213 | else:
214 | searchr = asslen - (i + 1)
215 | if re.search(style_read, '\n'.join(fullass[i + 1:searchr])) is None:
216 | break
217 | else:
218 | continue
219 | styleStr = ''.join([s.strip(' ') for s in fullass[i].split(':')[1:]]).strip(' ').split(',')
220 | font_key = styleStr[font_pos].lstrip('@')
221 | isItalic = - int(styleStr[italic_pos])
222 | isBold = - int(styleStr[bold_pos])
223 | fontlist.setdefault('{0}?{1}?{2}'.format(font_key, isItalic, isBold), '')
224 | style_font[styleStr[style_pos]] = '{0}?{1}?{2}'.format(font_key, isItalic, isBold)
225 |
226 | #print(fontlist)
227 |
228 | # 提取Event的 Format 行,并用半角逗号分割
229 | event_format = ''.join(fullass[eventline + 1].split(':')[1:]).strip(' ').split(',')
230 | # 确定Event中 Style 和 Text 的位置
231 | for i in range(0, len(event_format)):
232 | if event_format[i].lower().replace('\n', '').strip(' ') == 'style':
233 | style_pos2 = i
234 | elif event_format[i].lower().replace('\n', '').strip(' ') == 'text':
235 | text_pos = i
236 | if style_pos2 != 0 and text_pos != 0:
237 | break
238 |
239 | # 获取 字体的字符集
240 | # 先获取 Style,用style_font词典查找对应的 Font
241 | # 再将字符串追加到 fontlist 中对应 Font 的值中
242 | for i in range(eventline + 2, asslen):
243 | eventline_sp = fullass[i].split(':')
244 | if len(eventline_sp) < 2:
245 | if i + 1 > asslen:
246 | break
247 | else:
248 | if asslen - (i + 1) > 30:
249 | searchr = i + 31
250 | else:
251 | searchr = asslen - (i + 1)
252 | if re.search(event_read, '\n'.join(fullass[i + 1:searchr])) is None:
253 | break
254 | else:
255 | continue
256 | #print(fullass[i])
257 | if eventline_sp[0].strip(' ').lower() == 'comment':
258 | continue
259 | eventline_sp = ''.join(eventline_sp[1:]).split(',')
260 | eventftext = ','.join(eventline_sp[text_pos:])
261 | effectDel = r'(\{.*?\})|(\\[hnN])|(\s)'
262 | textremain = ''
263 | # 矢量绘图处理,如果发现有矢量表达,从字符串中删除这一部分
264 | if re.search(r'\{.*?\\p[1-9][0-9]*.*?\}([\s\S]*?)\{.*?\\p0.*?\}', eventftext) is not None:
265 | vecpos = re.findall(r'\{.*?\\p[1-9][0-9]*.*?\}[\s\S]*?\{.*?\\p0.*?\}', eventftext)
266 | vecfind = 0
267 | nexts = 0
268 | for s in vecpos:
269 | vecfind = eventftext.find(s)
270 | textremain += eventftext[nexts:vecfind]
271 | nexts = vecfind
272 | s = re.sub(r'\\p[0-9]+', '', re.sub(r'}.*?{', '}{', s))
273 | textremain += s
274 | elif re.search(r'\{.*?\\p[1-9][0-9]*.*?\}', eventftext) is not None:
275 | eventftext = re.sub(r'\\p[0-9]+', '', eventftext[:re.search(r'\{.*?\\p[1-9][0-9]*.*?\}', eventftext).span()[0]])
276 | if len(textremain) > 0:
277 | eventftext = textremain
278 | eventfont = style_font.get(eventline_sp[style_pos2].lstrip('*'))
279 |
280 | # 粗体、斜体标签处理
281 | splittext = []
282 | if re.search(r'\{.*?(?:\\b[7-9]00|\\b1|\\i1).*?\}', eventftext) is not None:
283 | lastfind = 0
284 | for st in re.findall(r'\{.*?\}', eventftext):
285 | ibclose = re.search(r'(\\b[1-4]00|\\b0|\\i0|\\b[\\\}]|\\i[\\\}])', st)
286 | ibopen = re.search(r'(\\b[7-9]00|\\b1|\\i1)', st)
287 | if ibopen is not None:
288 | stfind = eventftext.find(st)
289 | addbold = '0'
290 | additalic = '0'
291 | if re.search(r'(\\b[7-9]00|\\b1)', st) is not None:
292 | if re.search(r'(\\b[7-9]00|\\b1)', st).span()[0] > max([st.find('\\b0'), st.find('\\b\\'), st.find('\\b}')]):
293 | addbold = '1'
294 | if st.find('\\i1') > st.find('\\i0'): additalic = '1'
295 | if len(splittext) == 0:
296 | if stfind > 0:
297 | splittext.append([eventftext[:stfind], '0', '0'])
298 | splittext.append([eventftext[stfind:], additalic, addbold])
299 | else:
300 | if splittext[-1][1] != additalic or splittext[-1][2] != addbold:
301 | if stfind > 0:
302 | splittext[-1][0] = eventftext[lastfind:stfind]
303 | splittext.append([eventftext[stfind:], additalic, addbold])
304 | lastfind = stfind
305 | elif ibclose is not None:
306 | stfind = eventftext.find(st)
307 | if len(splittext) > 0:
308 | ltext = splittext[-1]
309 | readytext = [eventftext[stfind:], ltext[1], ltext[2]]
310 | if re.search(r'(\\i0|\\i[\\\}])', st) is not None:
311 | readytext[1] = '0'
312 | if re.search(r'(\\b[1-4]00|\\b0|\\b[\\\}])', st) is not None:
313 | readytext[2] = '0'
314 | if ltext[1] != readytext[1] or ltext[2] != readytext[2]:
315 | if stfind > 0:
316 | splittext[-1][0] = eventftext[lastfind:stfind]
317 | splittext.append(readytext)
318 | lastfind = stfind
319 | else:
320 | splittext.append([eventftext, '0', '0'])
321 | else: continue
322 | if len(splittext) == 0:
323 | splittext.append([eventftext, '0', '0'])
324 | # elif len(splittext) > 1:
325 | # print(splittext)
326 | splitpos = [[0, len(splittext[0][0]), int(splittext[0][1]), int(splittext[0][2])]]
327 | if len(splittext) > 1:
328 | for spi in range(1, len(splittext)):
329 | spil = len(splittext[spi][0])
330 | lspil = splitpos[spi - 1][1]
331 | splitpos.append([lspil, spil + lspil, int(splittext[spi][1]), int(splittext[spi][2])])
332 | # print(splitpos)
333 | # print(eventftext)
334 | # os.system('pause')
335 | del splittext
336 | # print(eventftext)
337 | # 字体标签分析
338 | eventftext2 = eventftext
339 | it = re.search(r'\{.*?\\fn.*?\}', eventftext)
340 | if it is not None:
341 | fn = ''
342 | fn_line = [i]
343 | while it is not None:
344 | it = it.span()
345 | if it[0] > 0:
346 | cuttext = eventftext[:it[0]]
347 | fnpos = eventftext2.find(cuttext)
348 | newstrl = []
349 | for sp in splitpos:
350 | if fnpos in range(sp[0], sp[1]):
351 | if fnpos + len(cuttext) not in range(sp[0], sp[1]):
352 | newstrl.append([eventftext2[fnpos:sp[1]], str(sp[2]), str(sp[3])])
353 | else:
354 | newstrl.append([cuttext, str(sp[2]), str(sp[3])])
355 | elif fnpos + len(cuttext) in range(sp[0], sp[1]):
356 | newstrl.append([eventftext2[sp[0]:(fnpos + len(cuttext))], str(sp[2]), str(sp[3])])
357 | for fi in range(0, len(newstrl)):
358 | fs = newstrl[fi]
359 | fss = re.sub(effectDel, '', fs[0])
360 | if len(fn) > 0:
361 | fontlist = fontlistAdd(fss, '?'.join([fn, fs[1], fs[2]]), fontlist)
362 | else: fontlist = fontlistAdd(fss, eventfont, fontlist)
363 | # s = re.sub(effectDel, '', eventftext[:it[0]])
364 | # print('add', s)
365 | # print('fn', fn)
366 | s = eventftext[(it[0] + 1):(it[1] - 1)]
367 | l = [sf.strip(' ') for sf in s.split('\\') if len(s.strip(' ')) > 0]
368 | l.reverse()
369 | for sf in l:
370 | if 'fn' in sf.lower():
371 | fn = sf[2:].strip(' ').lstrip('@')
372 | fn_line.append(s)
373 | break
374 | eventftext = eventftext[it[1]:]
375 | # print('ef', eventftext)
376 | it = re.search(r'\{\\fn.*?\}', eventftext)
377 | # os.system('pause')
378 | else:
379 | fnpos = eventftext2.find(eventftext)
380 | newstrl = []
381 | for sp in splitpos:
382 | if fnpos in range(sp[0], sp[1]):
383 | if fnpos + len(eventftext) not in range(sp[0], sp[1]):
384 | newstrl.append([eventftext2[fnpos:sp[1]], str(sp[2]), str(sp[3])])
385 | else:
386 | newstrl.append([eventftext, str(sp[2]), str(sp[3])])
387 | elif fnpos + len(eventftext) in range(sp[0], sp[1]):
388 | newstrl.append([eventftext2[sp[0]:(fnpos + len(eventftext))], str(sp[2]), str(sp[3])])
389 | for fi in range(0, len(newstrl)):
390 | fs = newstrl[fi]
391 | fss = re.sub(effectDel, '', fs[0])
392 | if len(fn) > 0:
393 | fontlist = fontlistAdd(fss, '?'.join([fn, fs[1], fs[2]]), fontlist)
394 | else: fontlist = fontlistAdd(fss, eventfont, fontlist)
395 | fn_lines.append(fn_line)
396 | else:
397 | if not eventfont is None:
398 | # 去除行中非文本部分,包括特效标签{},硬软换行符
399 | eventtext = re.sub(effectDel, '', eventftext)
400 | fontlist = fontlistAdd(eventtext, eventfont, fontlist)
401 |
402 | if not onlycheck: print('\033[1m字幕所需字体\033[0m')
403 | fl_popkey = []
404 | # 在字体列表中检查是否有没有在文本中使用的字体,如果有,添加到删去列表
405 | for s in fontlist.keys():
406 | if len(fontlist[s]) == 0:
407 | fl_popkey.append(s)
408 | #print('跳过没有字符的字体\"{0}\"'.format(s))
409 | else:
410 | if not onlycheck: print('\033[1m\"{0}\"\033[0m: 字符数[\033[1;33m{1}\033[0m]'.format(s, len(fontlist[s])))
411 | # 删去 删去列表 中的字体
412 | if len(fl_popkey) > 0:
413 | for s in fl_popkey:
414 | fontlist.pop(s)
415 | # print(fontlist)
416 | # 如果 onlycheck 为 True,只返回字体列表
417 | if onlycheck:
418 | del fullass
419 | style_font.clear()
420 | return None, fontlist, None, None, None
421 |
422 | #os.system('pause')
423 | return fullass, fontlist, styleline, font_pos, fn_lines
424 |
425 | # 获取字体文件列表
426 | # 接受输入
427 | # customPath: 用户指定的字体文件夹
428 | # font_name: 用于更新font_name(启用从注册表读取名称的功能时有效)
429 | # noreg: 只从用户提供的customPath获取输入
430 | # 将会返回
431 | # filelist: 字体文件清单 [[ 字体绝对路径, 读取位置('': 注册表, '0': 自定义目录) ], ...]
432 | # font_name: 用于更新font_name(启用从注册表读取名称的功能时有效)
433 | def getFileList(customPath: list = [], font_name: dict = {}, noreg: bool = False):
434 | filelist = []
435 |
436 | if not noreg:
437 | # 从注册表读取
438 | fontkey = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, r'SOFTWARE\Microsoft\Windows NT\CurrentVersion\Fonts')
439 | fontkey_num = winreg.QueryInfoKey(fontkey)[1]
440 | #fkey = ''
441 | try:
442 | # 从用户字体注册表读取
443 | fontkey10 = winreg.OpenKey(winreg.HKEY_CURRENT_USER, r'SOFTWARE\Microsoft\Windows NT\CurrentVersion\Fonts')
444 | fontkey10_num = winreg.QueryInfoKey(fontkey10)[1]
445 | if fontkey10_num > 0:
446 | for i in range(fontkey10_num):
447 | p = winreg.EnumValue(fontkey10, i)[1]
448 | #n = winreg.EnumValue(fontkey10, i)[0]
449 | if path.exists(p):
450 | # test = n.split('&')
451 | # if len(test) > 1:
452 | # for i in range(0, len(test)):
453 | # font_name[re.sub(r'\(.*?\)', '', test[i].strip(' '))] = [p, i]
454 | # else: font_name[re.sub(r'\(.*?\)', '', n.strip(' '))] = [p, 0]
455 | filelist.append([p, ''])
456 | # test = path.splitext(path.basename(p))[0].split('&')
457 | except:
458 | pass
459 | for i in range(fontkey_num):
460 | # 从 系统字体注册表 读取
461 | k = winreg.EnumValue(fontkey, i)[1]
462 | #n = winreg.EnumValue(fontkey, i)[0]
463 | pk = path.join(r'C:\Windows\Fonts', k)
464 | if path.exists(pk):
465 | # test = n.split('&')
466 | # if len(test) > 1:
467 | # for i in range(0, len(test)):
468 | # font_name[re.sub(r'\(.*?\)', '', test[i].strip(' '))] = [pk, i]
469 | # else: font_name[re.sub(r'\(.*?\)', '', n.strip(' '))] = [pk, 0]
470 | filelist.append([pk, ''])
471 |
472 | # 从定义的文件夹读取
473 | # fontspath = [r'C:\Windows\Fonts', path.join(os.getenv('USERPROFILE'),r'AppData\Local\Microsoft\Windows\Fonts')]
474 | if customPath is None: customPath == []
475 | if len(customPath) > 0:
476 | print('\033[1;33m请稍等,正在获取自定义文件夹中的字体\033[0m')
477 | for s in customPath:
478 | if not path.isdir(s): continue
479 | for r, d, f in os.walk(s):
480 | for p in f:
481 | p = path.join(r, p)
482 | if path.splitext(p)[1][1:].lower() not in ['ttf', 'ttc', 'otc', 'otf']: continue
483 | filelist.append([path.join(s, p), 'xxx'])
484 |
485 | #print(font_name)
486 | #os.system('pause')
487 | return filelist, font_name
488 |
489 | #字体处理部分
490 | # 需要输入
491 | # fl: 字体文件列表
492 | # 可选输入
493 | # f_n: 默认新建一个,可用于更新font_name
494 | # 将会返回
495 | # font_name: 字体内部名称与绝对路径的索引词典
496 | # 会对以下全局变量进行变更
497 | # dupfont: 重复字体的名称与其路径词典
498 | # font_n_lower: 字体全小写名称与其标准名称对应词典
499 |
500 | # font_name 词典结构
501 | # { 字体名称 : [ 字体绝对路径 , 字体索引 (仅用于TTC/OTC; 如果是TTF/OTF,默认为0) ] }
502 | # dupfont 词典结构
503 | # { 重复字体名称 : [ 字体1绝对路径, 字体2绝对路径, ... ] }
504 | # bold_font 列表、italic_font 列表结构
505 | # [ 字体名称 ]
506 | def fontProgress(fl: list, f_n: dict = {}) -> dict:
507 | global dupfont, font_family
508 | global font_n_lower
509 | #print(fl)
510 | flL = len(fl)
511 | print('\033[1;32m正在读取字体信息...\033[0m')
512 | for si in range(0, flL):
513 | s = fl[si][0]
514 | fromCustom = False
515 | if len(fl[si][1]) > 0: fromCustom = True
516 | # 如果有来自自定义文件夹标记,则将 fromCustom 设为 True
517 | ext = path.splitext(s)[1][1:]
518 | # 检查字体扩展名
519 | if ext.lower() not in ['ttf','ttc','otf','otc']: continue
520 | # 输出进度
521 | print('\r' + '\033[1;32m{0}/{1} {2:.2f}% \033[0m'.format(si + 1, flL, ((si + 1)/flL)*100, ), end='', flush=True)
522 | if ext.lower() in ['ttf', 'otf']:
523 | # 如果是 TTF/OTF 单字体文件,则使用 TTFont 读取
524 | try:
525 | tc = [ttLib.TTFont(s, lazy=True)]
526 | except:
527 | print('\033[1;31m\n[ERROR] \"{0}\": {1}\n\033[1;34m[TRY] 正在尝试使用TTC/OTC模式读取\033[0m'.format(s, sys.exc_info()))
528 | # 如果 TTFont 读取失败,可能是使用了错误扩展名的 TTC/OTC 文件,换成 TTCollection 尝试读取
529 | try:
530 | tc = ttLib.TTCollection(s, lazy=True)
531 | print('\033[1;34m[WARNING] 错误的字体扩展名\"{0}\" \033[0m'.format(s))
532 | except:
533 | print('\033[1;31m\n[ERROR] \"{0}\": {1}\033[0m'.format(s, sys.exc_info()))
534 | continue
535 | else:
536 | try:
537 | # 如果是 TTC/OTC 字体集合文件,则使用 TTCollection 读取
538 | tc = ttLib.TTCollection(s, lazy=True)
539 | except:
540 | print('\033[1;31m\n[ERROR] \"{0}\": {1}\n\033[1;34m[TRY] 正在尝试使用TTF/OTF模式读取\033[0m'.format(s, sys.exc_info()))
541 | try:
542 | # 如果读取失败,可能是使用了错误扩展名的 TTF/OTF 文件,用 TTFont 尝试读取
543 | tc = [ttLib.TTFont(s, lazy=True)]
544 | print('\033[1;34m[WARNING] 错误的字体扩展名\"{0}\" \033[0m'.format(s))
545 | except:
546 | print('\033[1;31m\n[ERROR] \"{0}\": {1}\033[0m'.format(s, sys.exc_info()))
547 | continue
548 | #f_n[path.splitext(path.basename(s))[0]] = [s, 0]
549 | for ti in range(0, len(tc)):
550 | t = tc[ti]
551 | # 读取字体的 'OS/2' 表的 'fsSelection' 项查询字体的粗体斜体信息
552 | os_2 = bin(t['OS/2'].fsSelection)[2:].zfill(10)
553 | isItalic = int(os_2[-1])
554 | isBold = int(os_2[-6])
555 | # isRegular = int(os_2[-7])
556 | # 读取字体的 'name' 表
557 | familyN = ''
558 | for ii in range(0, len(t['name'].names)):
559 | name = t['name'].names[ii]
560 | # 若 nameID 为 1,读取 NameRecord 的字体家族名称
561 | if name.nameID == 1:
562 | try:
563 | familyN = name.toStr()
564 | except:
565 | familyN = name.toBytes().decode('utf-16-be', errors='ignore')
566 | try:
567 | if len([i for i in name.toBytes() if i == 0]) > 0:
568 | nnames = t['name']
569 | namebyte = b''.join([bytes.fromhex('{:0>2}'.format(hex(i)[2:])) for i in name.toBytes() if i > 0])
570 | nnames.setName(namebyte,
571 | name.nameID, name.platformID, name.platEncID, name.langID)
572 | familyN = nnames.names[ii].toStr()
573 | else: familyN = name.toBytes().decode('utf-16-be', errors='ignore')
574 | except:
575 | pass
576 | font_family.setdefault(familyN.strip(' '), {})
577 | # 若 nameID 为 4,读取 NameRecord 的字体完整名称
578 | if name.nameID == 4:
579 | namestr = ''
580 | try:
581 | namestr = name.toStr()
582 | except:
583 | # 如果 fontTools 解码失败,则尝试使用 utf-16-be 直接解码
584 | namestr = name.toBytes().decode('utf-16-be', errors='ignore')
585 | try:
586 | # 尝试使用 去除 \x00 字符 解码
587 | if len([i for i in name.toBytes() if i == 0]) > 0:
588 | nnames = t['name']
589 | namebyte = b''.join([bytes.fromhex('{:0>2}'.format(hex(i)[2:])) for i in name.toBytes() if i > 0])
590 | nnames.setName(namebyte,
591 | name.nameID, name.platformID, name.platEncID, name.langID)
592 | namestr = nnames.names[ii].toStr()
593 | print('\n\033[1;33m已修正字体\"{0}\"名称读取 >> \"{1}\"\033[0m'.format(path.basename(s), namestr))
594 | #os.system('pause')
595 | else: namestr = name.toBytes().decode('utf-16-be', errors='ignore')
596 | # 如果没有 \x00 字符,使用 utf-16-be 强行解码;如果有,尝试解码;如果解码失败,使用 utf-16-be 强行解码
597 | except:
598 | print('\n\033[1;33m尝试修正字体\"{0}\"名称读取 >> \"{1}\"\033[0m'.format(path.basename(s), namestr))
599 | if namestr is None: continue
600 | namestr = namestr.strip(' ')
601 | #print(namestr, path.basename(s))
602 | if f_n.get(namestr) is not None:
603 | # 如果发现列表中已有相同名称的字体,检测它的文件名、扩展名、父目录是否相同
604 | # 如果有一者不同且不来自自定义文件夹,添加到重复字体列表
605 | dupp = f_n[namestr][0]
606 | if dupp != s and path.splitext(path.basename(dupp))[0] != path.splitext(path.basename(s))[0] and not fromCustom:
607 | print('\033[1;35m[WARNING] 字体\"{0}\"与字体\"{1}\"的名称\"{2}\"重复!\033[0m'.format(path.basename(f_n[namestr][0]), path.basename(s), namestr))
608 | if dupfont.get(namestr) is not None:
609 | if s not in dupfont[namestr]:
610 | dupfont[namestr].append(s)
611 | else:
612 | dupfont[namestr] = [dupp, s]
613 | else:
614 | f_n[namestr] = [s, ti]
615 | font_n_lower[namestr.lower()] = namestr
616 | if len(familyN) > 0:
617 | if font_family[familyN].get((isItalic, isBold)) is not None:
618 | font_family[familyN][(isItalic, isBold)].append(namestr)
619 | else: font_family[familyN].setdefault((isItalic, isBold), [namestr])
620 | #f_n[namestr] = [s, ti]
621 | # if f_n.get(fname) is None: f_n[fname] = [[namestr], s]
622 | # #print(fname, name.toStr(), f_n.get(fname))
623 | # if namestr not in f_n[fname][0]:
624 | # f_n[fname][0] = f_n[fname][0] + [namestr]
625 | tc[0].close()
626 | keys = list(font_family.keys())
627 | for k in keys:
628 | if not len(font_family[k]) > 1:
629 | font_family.pop(k)
630 | del keys
631 | return f_n
632 |
633 | #print(filelist)
634 | #if path.exists(fontspath10): filelist = filelist.extend(os.listdir(fontspath10))
635 |
636 | #for s in font_name.keys(): print('{0}: {1}'.format(s, font_name[s]))
637 |
638 | # 系统字体完整性检查,检查是否有ASS所需的全部字体,如果没有,则要求拖入
639 | # 需要以下输入
640 | # fontlist: 字体与其所需字符(只读字体部分) { ASS内的字体名称 : 字符串 }
641 | # font_name: 字体名称与字体路径对应词典 { 字体名称 : [ 字体绝对路径, 字体索引 ] }
642 | # 以下输入可选
643 | # assfont: 结构见下,用于多ASS文件时更新列表
644 | # onlycheck: 缺少字体时不要求输入
645 | # 将会返回以下
646 | # assfont: { 字体绝对路径?字体索引 : [ 字符串, ASS内的字体名称, 修正名称 ]}
647 | # font_name: 同上,用于有新字体拖入时对该词典的更新
648 | def checkAssFont(fontlist: dict, font_name: dict, assfont: dict = {}, onlycheck: bool = False):
649 | # 从fontlist获取字体名称
650 | global font_n_lower, font_family
651 | keys = list(fontlist.keys())
652 | for s in keys:
653 | sp = s.split('?')
654 | isbold = int(sp[2])
655 | isitalic = int(sp[1])
656 | ns = sp[0]
657 | if font_family.get(sp[0]) is not None:
658 | if font_family[sp[0]].get((isitalic, isbold)) is not None:
659 | ns = font_family[sp[0]][(isitalic, isbold)][0]
660 | elif font_family[sp[0]].get((0, isbold)) is not None:
661 | ns = font_family[sp[0]][(0, isbold)][0]
662 | elif font_family[sp[0]].get((isitalic, 0)) is not None:
663 | ns = font_family[sp[0]][(isitalic, 0)][0]
664 | elif font_family[sp[0]].get((0, 0)) is not None:
665 | ns = font_family[sp[0]][(0, 0)][0]
666 | cok = False
667 | # 在全局字体名称词典中寻找字体名称
668 | ss = ns
669 | if ns not in font_name:
670 | # 如果找不到,将字体名称统一为小写再次查找
671 | if font_n_lower.get(ns.lower()) is not None:
672 | ss = font_n_lower[ns.lower()]
673 | cok = True
674 | else: cok = True
675 | directout = 0
676 | if not cok:
677 | # 如果 onlycheck 不为 True,向用户要求目标字体
678 | if not onlycheck:
679 | print('\033[1;31m[ERROR] 缺少字体\"{0}\"\n请输入追加的字体文件或其所在字体目录的绝对路径\033[0m'.format(s))
680 | addFont = {}
681 | inFont = ''
682 | while inFont == '' and directout < 3:
683 | inFont = input().strip('\"').strip(' ')
684 | if path.exists(inFont):
685 | if path.isdir(inFont):
686 | addFont = fontProgress(getFileList([inFont], noreg=True)[0])
687 | else:
688 | addFont = fontProgress([[inFont, '0']])
689 | if s not in addFont.keys():
690 | if path.isdir(inFont):
691 | print('\033[1;31m[ERROR] 输入路径中\"{0}\"没有所需字体\"{1}\"\033[0m'.format(inFont, s))
692 | else: print('\033[1;31m[ERROR] 输入字体\"{0}\"不是所需字体\"{1}\"\033[0m'.format('|'.join(addFont.keys()), s))
693 | inFont = ''
694 | else:
695 | font_name.update(addFont)
696 | cok = True
697 | else:
698 | print('\033[1;31m[ERROR] 您没有输入任何字符!再回车{0}次回到主菜单\033[0m'.format(3-directout))
699 | directout += 1
700 | inFont = ''
701 | else:
702 | # 否则直接添加空
703 | assfont['?'.join([ns, ns])] = ['', sp[0], ns]
704 | if cok and directout < 3:
705 | # 如果找到,添加到assfont列表
706 | font_path = font_name[ss][0]
707 | font_index = font_name[ss][1]
708 | # print(font_name[ss])
709 | dict_key = '?'.join([font_path, str(font_index)])
710 | # 如果 assfont 列表已有该字体,则将新字符添加到 assfont 中
711 | if assfont.get(dict_key) is None:
712 | assfont[dict_key] = [fontlist[s], sp[0], ns]
713 | else:
714 | tfname = assfont[dict_key][2]
715 | newfnamep = assfont[dict_key][1].split('|')
716 | oldstr = fontlist[s]
717 | for newfname in newfnamep:
718 | if sp[0].lower() not in '|'.join(newfnamep).lower():
719 | key1 = 0
720 | key2 = 0
721 | for ii in [0, 1]:
722 | key1 = ii
723 | key2 = 0
724 | if fontlist.get('?'.join([newfname, str(key1), str(0)])) is not None:
725 | break
726 | elif fontlist.get('?'.join([newfname, str(key1), str(1)])) is not None:
727 | key2 = 1
728 | break
729 | newfstr = fontlist['?'.join([newfname, str(key1), str(key2)])]
730 | newfname = '|'.join([sp[0], newfname])
731 | for i in range(0, len(newfstr)):
732 | if newfstr[i] not in oldstr:
733 | oldstr += newfstr[i]
734 | else:
735 | newstr = assfont[dict_key][0]
736 | for i in range(0, len(newstr)):
737 | if newstr[i] not in oldstr:
738 | oldstr += newstr[i]
739 | if ns.lower() not in tfname.lower():
740 | tfname = '|'.join([ns, tfname])
741 | assfont[dict_key] = [oldstr, newfname, tfname]
742 | fontlist[s] = oldstr
743 | # else:
744 | # assfont[dict_key] = [fontlist[s], s]
745 | #print(assfont[dict_key])
746 | elif directout >= 3:
747 | return None, font_name
748 | # print(assfont)
749 | return assfont, font_name
750 |
751 | # print('正在输出字体子集字符集')
752 | # for s in fontlist.keys():
753 | # logpath = '{0}_{1}.log'.format(path.join(os.getenv('TEMP'), path.splitext(path.basename(asspath))[0]), s)
754 | # log = open(logpath, mode='w', encoding='utf-8')
755 | # log.write(fontlist[s])
756 | # log.close()
757 |
758 | # 字体内部名称变更
759 | def getNameStr(name, subfontcrc: str) -> str:
760 | namestr = ''
761 | nameID = name.nameID
762 | # 变更NameID为1, 3, 4, 6的NameRecord,它们分别对应
763 | # ID Meaning
764 | # 1 Font Family name
765 | # 3 Unique font identifier
766 | # 4 Full font name
767 | # 6 PostScript name for the font
768 | # 注意本脚本并不更改 NameID 为 0 和 7 的版权信息
769 | if nameID in [1,3,4,6]:
770 | namestr = subfontcrc
771 | else:
772 | try:
773 | namestr = name.toStr()
774 | except:
775 | namestr = name.toBytes().decode('utf-16-be', errors='ignore')
776 | return namestr
777 |
778 | # 字体子集化
779 | # 需要以下输入:
780 | # assfont: { 字体绝对路径?字体索引 : [ 字符串, ASS内的字体名称 ]}
781 | # fontdir: 新字体存放目录
782 | # 将会返回以下:
783 | # newfont_name: { 原字体名 : [ 新字体绝对路径, 新字体名 ] }
784 | def assFontSubset(assfont: dict, fontdir: str) -> dict:
785 | newfont_name = {}
786 | # print(fontdir)
787 |
788 | if path.exists(path.dirname(fontdir)):
789 | if not path.isdir(fontdir):
790 | try:
791 | os.mkdir(fontdir)
792 | except:
793 | print('\033[1;31m[ERROR] 创建文件夹\"{0}\"失败\033[0m'.format(fontdir))
794 | fontdir = os.getcwd()
795 | if not path.isdir(fontdir): fontdir = path.dirname(fontdir)
796 | else: fontdir = os.getcwd()
797 | print('\033[1;33m字体输出路径:\033[0m \033[1m\"{0}\"\033[0m'.format(fontdir))
798 |
799 | lk = len(assfont.keys())
800 | kip = 0
801 | for k in assfont.keys():
802 | kip += 1
803 | # 偷懒没有变更该函数中的assfont解析到新的词典格式
804 | # 在这里会将assfont词典转换为旧的assfont列表形式
805 | # assfont: [ 字体绝对路径, 字体索引, 字符串, ASS内的字体名称, 修正字体名称 ]
806 | s = k.split('?') + [assfont[k][0], assfont[k][1], assfont[k][2]]
807 | subfontext = ''
808 | fontext = path.splitext(path.basename(s[0]))[1]
809 | if fontext[1:].lower() in ['otc', 'ttc']:
810 | subfontext = fontext[:3].lower() + 'f'
811 | else: subfontext = fontext
812 | #print(fontdir, path.exists(path.dirname(fontdir)), path.exists(fontdir))
813 | fontname = re.sub(cillegal, '_', s[4])
814 | subfontpath = path.join(fontdir, fontname + subfontext)
815 | subsetarg = [s[0], '--text={0}'.format(s[2]), '--output-file={0}'.format(subfontpath), '--font-number={0}'.format(s[1]), '--passthrough-tables']
816 | print('\r\033[1;32m[{0}/{1}]\033[0m \033[1m正在子集化…… \033[0m'.format(kip, lk), end='')
817 | try:
818 | subset.main(subsetarg)
819 | except PermissionError:
820 | print('\n\033[1;31m[ERROR] 文件\"{0}\"访问失败\033[0m'.format(path.basename(subfontpath)))
821 | continue
822 | except:
823 | # print('\033[1;31m[ERROR] 失败字符串: \"{0}\" \033[0m'.format(s[2]))
824 | print('\n\033[1;31m[ERROR] {0}\033[0m'.format(sys.exc_info()))
825 | print('\033[1;31m[WARNING] 字体\"{0}\"子集化失败,将会保留完整字体\033[0m'.format(path.basename(s[0])))
826 | # crcnewf = ''.join([path.splitext(subfontpath)[0], fontext])
827 | # shutil.copy(s[0], crcnewf)
828 | ttLib.TTFont(s[0], lazy=False, fontNumber=int(s[1])).save(subfontpath, False)
829 | subfontcrc = None
830 | # newfont_name[s[3]] = [crcnewf, subfontcrc]
831 | newfont_name[s[3]] = [subfontpath, subfontcrc]
832 | continue
833 | #os.system('pyftsubset {0}'.format(' '.join(subsetarg)))
834 | if path.exists(subfontpath):
835 | subfontbyte = open(subfontpath, mode='rb')
836 | subfontcrc = str(hex(zlib.crc32(subfontbyte.read())))[2:].upper()
837 | if len(subfontcrc) < 8: subfontcrc = '0' + subfontcrc
838 | # print('CRC32: {0} \"{1}\"'.format(subfontcrc, path.basename(s[0])))
839 | subfontbyte.close()
840 | rawf = ttLib.TTFont(s[0], lazy=True, fontNumber=int(s[1]))
841 | newf = ttLib.TTFont(subfontpath, lazy=False)
842 | if len(newf['name'].names) == 0:
843 | for i in range(0,7):
844 | if len(rawf['name'].names) - 1 >= i:
845 | name = rawf['name'].names[i]
846 | namestr = getNameStr(name, subfontcrc)
847 | newf['name'].addName(namestr, minNameID=-1)
848 | else:
849 | for i in range(0, len(rawf['name'].names)):
850 | name = rawf['name'].names[i]
851 | namestr = getNameStr(name, subfontcrc)
852 | newf['name'].setName(namestr ,name.nameID, name.platformID, name.platEncID, name.langID)
853 | if len(newf.getGlyphOrder()) == 1 and '.notdef' in newf.getGlyphOrder():
854 | print('\n\033[1;31m[WARNING] 字体\"{0}\"子集化失败,将会保留完整字体\033[0m'.format(path.basename(s[0])))
855 | crcnewf = subfontpath
856 | newf.close()
857 | if not subfontpath == s[0]: os.remove(subfontpath)
858 | # shutil.copy(s[0], crcnewf)
859 | rawf.save(crcnewf, False)
860 | subfontcrc = None
861 | else:
862 |
863 | crcnewf = '.{0}'.format(subfontcrc).join(path.splitext(subfontpath))
864 | newf.save(crcnewf)
865 | newf.close()
866 | rawf.close()
867 | if path.exists(crcnewf):
868 | if not subfontpath == crcnewf: os.remove(subfontpath)
869 | newfont_name[s[3]] = [crcnewf, subfontcrc]
870 | print('')
871 | #print(newfont_name)
872 | return newfont_name
873 |
874 | # 更改ASS样式对应的字体
875 | # 需要以下输入
876 | # fullass: 完整的ass文件内容,以行分割为列表
877 | # newfont_name: { 原字体名 : [ 新字体路径, 新字体名 ] }
878 | # asspath: 原ass文件的绝对路径
879 | # styleline: [V4/V4+ Styles]标签在SSA/ASS中的行数,对应到fullass列表的索引数
880 | # font_pos: Font参数在 Styles 的 Format 中位于第几个逗号之后
881 | # 以下输入可选
882 | # outdir: 新字幕的输出目录,默认为源文件目录
883 | # ncover: 为True时不覆盖原有文件,为False时覆盖
884 | # fn_lines: 带有fn标签的行数,对应到fullass的索引
885 | # 将会返回以下
886 | # newasspath: 新ass文件的绝对路径
887 | def assFontChange(fullass: list, newfont_name: dict, asspath: str, styleline: int,
888 | font_pos: int, outdir: str = '', ncover: bool = False, fn_lines: list = []) -> str:
889 | # 扫描Style各行,并替换掉字体名称
890 | #print('正在替换style对应字体......')
891 | for i in range(styleline + 2, len(fullass)):
892 | if len(fullass[i].split(':')) < 2:
893 | if re.search(style_read, '\n'.join(fullass[i + 1:])) is None:
894 | break
895 | else:
896 | continue
897 | styleStr = ''.join(fullass[i].split(':')[1:]).strip(' ').split(',')
898 | fontstr = styleStr[font_pos].lstrip('@')
899 | if not newfont_name.get(fontstr) is None:
900 | if not newfont_name[fontstr][1] is None:
901 | fullass[i] = fullass[i].replace(fontstr, newfont_name[fontstr][1])
902 | if len(fn_lines) > 0:
903 | #print('正在处理fn标签......')
904 | for fl in fn_lines:
905 | fn_line = fullass[fl[0]]
906 | for ti in range(1, len(fl)):
907 | for k in newfont_name.keys():
908 | if k in fl[ti]:
909 | fn_line = fn_line.replace(fl[ti], fl[ti].replace(k, newfont_name[k][1]))
910 | fullass[fl[0]] = fn_line
911 | if path.exists(path.dirname(outdir)):
912 | if not path.isdir(outdir):
913 | try:
914 | os.mkdir(outdir)
915 | except:
916 | print('\033[1;31m[ERROR] 创建文件夹\"{0}\"失败\033[0m'.format(outdir))
917 | outdir = os.getcwd()
918 | print('\033[1;33m字幕输出路径:\033[0m \033[1m\"{0}\"\033[0m'.format(outdir))
919 | if path.isdir(outdir):
920 | newasspath = path.join(outdir, '.subset'.join(path.splitext(path.basename(asspath))))
921 | else: newasspath = '.subset'.join(path.splitext(asspath))
922 | if path.exists(newasspath) and ncover:
923 | testpathl = path.splitext(newasspath)
924 | testc = 1
925 | testpath = '{0}#{1}{2}'.format(testpathl[0], testc, testpathl[1])
926 | while path.exists(testpath):
927 | testc += 1
928 | testpath = '{0}#{1}{2}'.format(testpathl[0], testc, testpathl[1])
929 | newasspath = testpath
930 | ass = open(newasspath, mode='w', encoding='utf-8')
931 | ass.writelines(fullass)
932 | ass.close()
933 | #print('ASS样式转换完成: {0}'.format(path.basename(newasspath)))
934 | return newasspath
935 |
936 | # ASFMKV,将媒体文件、字幕、字体封装到一个MKV文件,需要mkvmerge命令行支持
937 | # 需要以下输入
938 | # file: 媒体文件绝对路径
939 | # outfile: 输出文件的绝对路径,如果该选项空缺,默认为 输入媒体文件.muxed.mkv
940 | # asslangs: 赋值给字幕轨道的语言,如果字幕轨道多于asslangs的项目数,超出部分将全部应用asslangs的末项
941 | # asspaths: 字幕绝对路径列表
942 | # fontpaths: 字体列表,格式为 [[字体1绝对路径], [字体1绝对路径], ...],必须嵌套一层,因为主函数偷懒了
943 | # 将会返回以下
944 | # mkvmr: mkvmerge命令行的返回值
945 | def ASFMKV(file: str, outfile: str = '', asslangs: list = [], asspaths: list = [], fontpaths: list = []) -> int:
946 | #print(fontpaths)
947 | global rmAssIn, rmAttach, mkvout, notfont
948 | if file is None: return 4
949 | elif file == '': return 4
950 | elif not path.exists(file) or not path.isfile(file): return 4
951 | if outfile is None: outfile = ''
952 | if outfile == '' or not path.exists(path.dirname(outfile)) or path.dirname(outfile) == path.dirname(file):
953 | outfile = '.muxed'.join([path.splitext(file)[0], '.mkv'])
954 | outfile = path.splitext(outfile)[0] + '.mkv'
955 | if path.exists(outfile):
956 | checkloop = 1
957 | while path.exists('#{0}'.format(checkloop).join(path.splitext(outfile))):
958 | checkloop += 1
959 | outfile = '#{0}'.format(checkloop).join([path.splitext(outfile)[0], '.mkv'])
960 | mkvargs = []
961 | if rmAssIn: mkvargs.append('-S')
962 | if rmAttach: mkvargs.append('-M')
963 | mkvargs.extend(['(', file, ')'])
964 | fn = path.splitext(path.basename(file))[0]
965 | if len(asspaths) > 0:
966 | for i in range(0, len(asspaths)):
967 | s = asspaths[i]
968 | assfn = path.splitext(path.basename(s))[0]
969 | assnote = assfn[(assfn.find(fn) + len(fn)):].replace('.subset', '')
970 | #print(assfn, fn, assnote)
971 | if len(assnote) > 1:
972 | mkvargs.extend(['--track-name', '0:{0}'.format(assnote.lstrip('.'))])
973 | if len(asslangs) > 0 and path.splitext(s)[1][1:].lower() not in ['idx']:
974 | mkvargs.append('--language')
975 | if i < len(asslangs):
976 | mkvargs.append('0:{0}'.format(asslangs[i]))
977 | else:
978 | mkvargs.append('0:{0}'.format(asslangs[len(asslangs) - 1]))
979 | mkvargs.extend(['(', s, ')'])
980 | if len(fontpaths) > 0:
981 | for s in fontpaths:
982 | fext = path.splitext(s[0])[1][1:].lower()
983 | if fext in ['ttf', 'ttc']:
984 | mkvargs.extend(['--attachment-mime-type', 'application/x-truetype-font'])
985 | elif fext in ['otf', 'otc']:
986 | mkvargs.extend(['--attachment-mime-type', 'application/vnd.ms-opentype'])
987 | mkvargs.extend(['--attach-file', s[0]])
988 | mkvargs.extend(['--title', fn])
989 | mkvjsonp = path.splitext(file)[0] + '.mkvmerge.json'
990 | mkvjson = open(mkvjsonp, mode='w', encoding='utf-8')
991 | json.dump(mkvargs, fp=mkvjson, sort_keys=True, indent=2, separators=(',', ': '))
992 | mkvjson.close()
993 | mkvmr = os.system('mkvmerge @\"{0}\" -o \"{1}\"'.format(mkvjsonp, outfile))
994 | if mkvmr > 1:
995 | print('\n\033[1;31m[ERROR] 检测到不正常的mkvmerge返回值,重定向输出...\033[0m')
996 | os.system('mkvmerge -r \"{0}\" @\"{1}\" -o NUL'.format('{0}.{1}.log'
997 | .format(path.splitext(file)[0], datetime.now().strftime('%Y-%m%d-%H%M-%S_%f')), mkvjsonp))
998 | elif not notfont:
999 | for p in asspaths:
1000 | print('\033[1;32m封装成功: \033[1;37m\"{0}\"\033[0m'.format(p))
1001 | if path.splitext(p)[1][1:].lower() in ['ass', 'ssa']:
1002 | try:
1003 | os.remove(p)
1004 | except:
1005 | print('\033[1;33m[ERROR] 文件\"{0}\"删除失败\033[0m'.format(p))
1006 | for f in fontpaths:
1007 | print('\033[1;32m封装成功: \033[1;37m\"{0}\"\033[0m'.format(f[0]))
1008 | try:
1009 | os.remove(f[0])
1010 | except:
1011 | print('\033[1;33m[ERROR] 文件\"{0}\"删除失败\033[0m'.format(f[0]))
1012 | try:
1013 | os.remove(mkvjsonp)
1014 | except:
1015 | print('\033[1;33m[ERROR] 文件\"{0}\"删除失败\033[0m'.format(mkvjsonp))
1016 | print('\033[1;32m输出成功:\033[0m \033[1m\"{0}\"\033[0m'.format(outfile))
1017 | else:
1018 | print('\033[1;32m输出成功:\033[0m \033[1m\"{0}\"\033[0m'.format(outfile))
1019 | return mkvmr
1020 |
1021 | # 从输入的目录中获取媒体文件列表
1022 | # 需要以下输入
1023 | # dir: 要搜索的目录
1024 | # 返回以下结果
1025 | # medias: 多媒体文件列表
1026 | # 结构: [[ 文件名(无扩展名), 绝对路径 ], ...]
1027 | def getMediaFilelist(dir: str) -> list:
1028 | medias = []
1029 | global v_subdir, extlist
1030 | if path.isdir(dir):
1031 | if v_subdir:
1032 | for r,ds,fs in os.walk(dir):
1033 | for f in fs:
1034 | if path.splitext(f)[1][1:].lower() in extlist:
1035 | medias.append([path.splitext(path.basename(f))[0], path.join(r, f)])
1036 | else:
1037 | for f in os.listdir(dir):
1038 | if path.isfile(path.join(dir, f)):
1039 | if path.splitext(f)[1][1:].lower() in extlist:
1040 | medias.append([path.splitext(path.basename(f))[0], path.join(dir, f)])
1041 | return medias
1042 |
1043 | # 在目录中找到与媒体文件列表中的媒体文件对应的字幕
1044 | # 遵循以下原则
1045 | # 媒体文件在上级目录,则匹配子目录中的字幕;媒体文件的字幕只能在媒体文件的同一目录或子目录中,不能在上级目录和其他同级目录
1046 | # 需要以下输入
1047 | # medias: 媒体文件列表,结构见 getMediaFilelist
1048 | # cpath: 开始搜索的顶级目录
1049 | # 将会返回以下
1050 | # media_ass: 媒体文件与字幕文件的对应词典
1051 | # 结构: { 媒体文件绝对路径 : [ 字幕1绝对路径, 字幕2绝对路径, ...] }
1052 | def getSubtitles(medias: list, cpath: str) -> dict:
1053 | media_ass = {}
1054 | global s_subdir, matchStrict
1055 | if s_subdir:
1056 | for r,ds,fs in os.walk(cpath):
1057 | for f in [path.join(r, s) for s in fs if path.splitext(s)[1][1:].lower() in subext]:
1058 | if '.subset' in path.basename(f): continue
1059 | for l in medias:
1060 | vdir = path.dirname(l[1])
1061 | sdir = path.dirname(f)
1062 | sext = path.splitext(f)
1063 | if (l[0] in f and not matchStrict) or (l[0] == path.basename(f)[:len(l[0])] and matchStrict):
1064 | if((vdir in sdir and sdir not in vdir) or (vdir == sdir)):
1065 | if sext[1][:1].lower() == 'idx':
1066 | if not path.exists(sext[1] + '.sub'):
1067 | continue
1068 | if media_ass.get(l[1]) is None:
1069 | media_ass[l[1]] = [f]
1070 | else: media_ass[l[1]].append(f)
1071 | else:
1072 | for f in [path.join(cpath, s) for s in os.listdir(cpath) if not path.isdir(s) and
1073 | path.splitext(s)[1][1:].lower() in subext]:
1074 | # print(f, cpath)
1075 | if '.subset' in path.basename(f): continue
1076 | for l in medias:
1077 | # print(path.basename(f)[len(l[0]):], l)
1078 | sext = path.splitext(f)
1079 | if (l[0] in f and not matchStrict) or (l[0] == path.basename(f)[:len(l[0])] and matchStrict):
1080 | if path.dirname(l[1]) == path.dirname(f):
1081 | if sext[1][:1].lower() == 'idx':
1082 | if not path.exists(sext[1] + '.sub'):
1083 | continue
1084 | if media_ass.get(l[1]) is None:
1085 | media_ass[l[1]] = [f]
1086 | else: media_ass[l[1]].append(f)
1087 | return media_ass
1088 |
1089 | # 主函数,负责调用各函数走完完整的处理流程
1090 | # 需要以下输入
1091 | # font_name: 字体名称与字体路径对应词典,结构见 fontProgress
1092 | # asspath: 字幕绝对路径列表
1093 | # 以下输入可选
1094 | # outdir: 输出目录,格式 [ 字幕输出目录, 字体输出目录, 视频输出目录 ],如果项数不足,则取最后一项;默认为 asspaths 中每项所在目录
1095 | # mux: 不要封装,只运行到子集化完成
1096 | # vpath: 视频路径,只在 mux = True 时生效
1097 | # asslangs: 字幕语言列表,将会按照顺序赋给对应的字幕轨道,只在 mux = True 时生效
1098 | # 将会返回以下
1099 | # newasspath: 列表,新生成的字幕文件绝对路径
1100 | # newfont_name: 词典,{ 原字体名 : [ 新字体绝对路径, 新字体名 ] }
1101 | # ??? : 数值,mkvmerge的返回值;如果 mux = False,返回-1
1102 | def main(font_name: dict, asspath: list, outdir: list = ['', '', ''], mux: bool = False, vpath: str = '', asslangs: list = []):
1103 | print('')
1104 | outdir_temp = outdir[:3]
1105 | outdir = ['', '', '']
1106 | for i in range(0, len(outdir_temp)):
1107 | s = outdir_temp[i]
1108 | # print(s)
1109 | if s is None:
1110 | outdir[i] = ''
1111 | elif s == '':
1112 | outdir[i] = s
1113 | else:
1114 | try:
1115 | if not path.isdir(s) and path.exists(path.dirname(s)):
1116 | os.mkdir(s)
1117 | if path.isdir(s): outdir[i] = s
1118 | except:
1119 | print('\033[1;31m[ERROR] 创建输出文件夹错误\n[ERROR] {0}\033[0m'.format(sys.exc_info()))
1120 | if '\\' in s:
1121 | outdir[i] = path.join(os.getcwd(), path.basename(s.rstrip('\\')))
1122 | else: outdir[i] = path.join(os.getcwd(), s)
1123 | # print(outdir)
1124 | # os.system('pause')
1125 | global notfont
1126 | # multiAss 多ASS文件输入记录词典
1127 | # 结构: { ASS文件绝对路径 : [ 完整ASS文件内容(fullass), 样式位置(styleline), 字体在样式行中的位置(font_pos) ]}
1128 | multiAss = {}
1129 | assfont = {}
1130 | fontlist = {}
1131 | newasspath = []
1132 | fo = ''
1133 | if not notfont:
1134 | # print('\n字体名称总数: {0}'.format(len(font_name.keys())))
1135 | # noass = False
1136 | for i in range(0, len(asspath)):
1137 | s = asspath[i]
1138 | if path.splitext(s)[1][1:].lower() not in ['ass', 'ssa']:
1139 | multiAss[s] = [[], 0, 0]
1140 | else:
1141 | # print('正在分析字幕文件: \"{0}\"'.format(path.basename(s)))
1142 | fullass, fontlist, styleline, font_pos, fn_lines = assAnalyze(s, fontlist)
1143 | multiAss[s] = [fullass, styleline, font_pos]
1144 | assfont, font_name = checkAssFont(fontlist, font_name, assfont)
1145 | if assfont is None:
1146 | return None, None, -2
1147 | sn = path.splitext(path.basename(asspath[0]))[0]
1148 | fn = path.join(path.dirname(asspath[0]), 'Fonts')
1149 | if outdir[1] == '': outdir[1] = fn
1150 | if not path.isdir(outdir[1]):
1151 | try:
1152 | os.mkdir(outdir[1])
1153 | fo = path.join(outdir[1], sn)
1154 | except:
1155 | fo = path.join(path.dirname(outdir[1]), sn)
1156 | else:
1157 | fo = path.join(outdir[1], sn)
1158 | newfont_name = assFontSubset(assfont, fo)
1159 | for s in asspath:
1160 | if path.splitext(s)[1][1:].lower() not in ['ass', 'ssa']:
1161 | newasspath.append(s)
1162 | elif len(multiAss[s][0]) == 0 or multiAss[s][1] == multiAss[s][2]:
1163 | continue
1164 | else: newasspath.append(assFontChange(multiAss[s][0], newfont_name, s, multiAss[s][1], multiAss[s][2], outdir[0], fn_lines=fn_lines))
1165 | else:
1166 | newasspath = asspath
1167 | newfont_name = {}
1168 | if mux:
1169 | if outdir[2] == '': outdir[2] = path.dirname(vpath)
1170 | if not path.isdir(outdir[2]):
1171 | try:
1172 | os.mkdir(outdir[2])
1173 | except:
1174 | outdir[2] = path.dirname(outdir[2])
1175 | mkvr = ASFMKV(vpath, path.join(outdir[2], path.splitext(path.basename(vpath))[0] + '.mkv'),
1176 | asslangs=asslangs, asspaths=newasspath, fontpaths=list(newfont_name.values()))
1177 | if not notfont:
1178 | for ap in newasspath:
1179 | if path.exists(path.dirname(ap)) and path.splitext(ap)[1][1:].lower() not in ['ass', 'ssa']:
1180 | try:
1181 | os.rmdir(path.dirname(ap))
1182 | except:
1183 | break
1184 | for fp in newfont_name.keys():
1185 | if path.exists(path.dirname(newfont_name[fp][0])):
1186 | try:
1187 | os.rmdir(path.dirname(newfont_name[fp][0]))
1188 | except:
1189 | continue
1190 | if path.isdir(fo):
1191 | try:
1192 | os.rmdir(fo)
1193 | except:
1194 | pass
1195 | return newasspath, newfont_name, mkvr
1196 | else:
1197 | return newasspath, newfont_name, -1
1198 |
1199 | def cls():
1200 | os.system('cls')
1201 |
1202 |
1203 | # 初始化字体列表 和 mkvmerge 相关参数
1204 | os.system('title ASFMKV Python Remake 1.01 ^| (c) 2022 yyfll ^| Apache-2.0')
1205 | fontlist, font_name = getFileList(fontin)
1206 | font_name = fontProgress(fontlist, font_name)
1207 | no_mkvm = False
1208 | no_cmdc = False
1209 | mkvmv = '\n\033[1;33m没有检测到 mkvmerge\033[0m'
1210 | if os.system('mkvmerge -V 1>nul 2>nul') > 0:
1211 | no_mkvm = True
1212 | else:
1213 | print('\n\n\033[1;33m正在获取 mkvmerge 语言编码列表和支持格式列表,请稍等...\033[0m')
1214 | mkvmv = '\n' + os.popen('mkvmerge -V --ui-language en', mode='r').read().replace('\n', '')
1215 | extget = re.compile(r'\[.*\]')
1216 | langmkv = os.popen('mkvmerge --list-languages', mode='r')
1217 | for s in langmkv.buffer.read().decode('utf-8').splitlines()[2:]:
1218 | s = s.replace('\n', '').split('|')
1219 | for si in range(1, len(s)):
1220 | ss = s[si]
1221 | if len(ss.strip(' ')) > 0:
1222 | langlist.append(ss.strip(' '))
1223 | langmkv.close()
1224 | if not no_extcheck:
1225 | for s in os.popen('mkvmerge -l --ui-language en', mode='r').readlines()[1:]:
1226 | for ss in re.search(r'\[.*\]', s).group().lstrip('[').rstrip(']').split(' '):
1227 | extsupp.append(ss)
1228 | extl_c = extlist
1229 | extlist = []
1230 | print('')
1231 | for i in range(0, len(extl_c)):
1232 | s = extl_c[i]
1233 | if s in extsupp:
1234 | extlist.append(s)
1235 | else:
1236 | print('\033[1;31m[WARNING] 您设定的媒体扩展名 {0} 无效,已从列表移除\033[0m'.format(s))
1237 | if len(extlist) != len(extl_c):
1238 | print('\n\033[1;33m当前的媒体扩展名列表: \"{0}\"\033[0m\n'.format(';'.join(extlist)))
1239 | os.system('pause')
1240 | del extl_c
1241 | if len(sublang) > 0:
1242 | print('')
1243 | sublang_c = sublang
1244 | sublang = []
1245 | for i in range(0, len(sublang_c)):
1246 | s = sublang_c[i]
1247 | if s in langlist:
1248 | sublang.append(s)
1249 | else:
1250 | sublang.append('und')
1251 | print('\033[1;31m[WARNING] 您设定的语言编码 {0} 无效,已替换为und\033[0m'.format(s))
1252 | if len(sublang) != len(sublang_c):
1253 | print('\n\033[1;33m当前的语言编码列表: \"{0}\"\033[0m\n'.format(';'.join(sublang)))
1254 | os.system('pause')
1255 | del sublang_c
1256 |
1257 |
1258 | def cListAssFont(font_name):
1259 | global resultw, s_subdir, copyfont
1260 | leave = True
1261 | while leave:
1262 | cls()
1263 | print('''ASFMKV-ListAssFontPy
1264 | 注意: 本程序由于设计原因,列出的是字体文件与其内部字体名的对照表
1265 |
1266 | 选择功能:
1267 | [L] 回到上级菜单
1268 | [A] 列出全部字体
1269 | [B] 检查并列出字幕所需字体
1270 |
1271 | 切换开关:
1272 | [1] 将结果写入文件: \033[1;33m{0}\033[0m
1273 | [2] 拷贝所需字体: \033[1;33m{1}\033[0m
1274 | [3] 搜索子目录(字幕): \033[1;33m{2}\033[0m
1275 | '''.format(resultw, copyfont, s_subdir))
1276 | work = os.system('choice /M 请输入 /C AB123L')
1277 | if work == 1:
1278 | cls()
1279 | wfilep = path.join(os.getcwd(), datetime.now().strftime('ASFMKV_FullFont_%Y-%m%d-%H%M-%S_%f.log'))
1280 | if resultw:
1281 | wfile = open(wfilep, mode='w', encoding='utf-8-sig')
1282 | else: wfile = None
1283 | fn = ''
1284 | print('FontList', file=wfile)
1285 | for s in font_name.keys():
1286 | nfn = path.basename(font_name[s][0])
1287 | if fn != nfn:
1288 | if wfile is not None:
1289 | print('>\n{0} <{1}'.format(nfn, s), end='', file=wfile)
1290 | else: print('>\033[0m\n\033[1;36m{0}\033[0m \033[1m<{1}'.format(nfn, s), end='')
1291 | else:
1292 | print(', {0}'.format(s), end='', file=wfile)
1293 | fn = nfn
1294 | if wfile is not None:
1295 | print(wfilep)
1296 | print('>', file=wfile)
1297 | wfile.close()
1298 | else: print('>\033[0m')
1299 | elif work == 2:
1300 | cls()
1301 | cpath = ''
1302 | directout = False
1303 | while not path.exists(cpath) and not directout:
1304 | directout = True
1305 | cpath = input('请输入目录路径或字幕文件路径: ').strip('\"')
1306 | if cpath == '' : print('没有输入,回到上级菜单')
1307 | elif not path.exists(cpath): print('\033[1;31m[ERROR] 找不到路径: \"{0}\"\033[0m'.format(cpath))
1308 | elif not path.isfile(cpath) and not path.isdir(cpath): print('\033[1;31m[ERROR] 输入的必须是文件或目录!: \"{0}\"\033[0m'.format(cpath))
1309 | elif not path.isabs(cpath): print('\033[1;31m[ERROR] 路径必须为绝对路径!: \"{0}\"\033[0m'.format(cpath))
1310 | elif path.isdir(cpath): directout = False
1311 | elif not path.splitext(cpath)[1][1:].lower() in ['ass', 'ssa']:
1312 | print('\033[1;31m[ERROR] 输入的必须是ASS/SSA字幕文件!: \"{0}\"\033[0m'.format(cpath))
1313 | else: directout = False
1314 | #clist = []
1315 | fontlist = {}
1316 | assfont = {}
1317 | if not directout:
1318 | if path.isdir(cpath):
1319 | #print(cpath)
1320 | dir = ''
1321 | if s_subdir:
1322 | for r,ds,fs in os.walk(cpath):
1323 | for f in fs:
1324 | if path.splitext(f)[1][1:].lower() in ['ass', 'ssa']:
1325 | a, fontlist, b, c, d = assAnalyze(path.join(r, f), fontlist, onlycheck=True)
1326 | assfont, font_name = checkAssFont(fontlist, font_name, assfont, onlycheck=True)
1327 | else:
1328 | for f in os.listdir(cpath):
1329 | if path.isfile(path.join(cpath, f)):
1330 | #print(f, path.splitext(f)[1][1:].lower())
1331 | if path.splitext(f)[1][1:].lower() in ['ass', 'ssa']:
1332 | #print(f, 'pass')
1333 | a, fontlist, b, c, d = assAnalyze(path.join(cpath, f), fontlist, onlycheck=True)
1334 | assfont, font_name = checkAssFont(fontlist, font_name, assfont, onlycheck=True)
1335 | fd = path.join(cpath, 'Fonts')
1336 | else:
1337 | a, fontlist, b, c, d = assAnalyze(cpath, fontlist, onlycheck=True)
1338 | assfont, font_name = checkAssFont(fontlist, font_name, assfont, onlycheck=True)
1339 | fd = path.join(path.dirname(cpath), 'Fonts')
1340 | if len(assfont.keys()) < 1:
1341 | print('\033[1;31m[ERROR] 目标路径没有ASS/SSA字幕文件\033[0m')
1342 | else:
1343 | wfile = None
1344 | print('')
1345 | if copyfont or resultw:
1346 | if not path.isdir(fd): os.mkdir(fd)
1347 | if resultw:
1348 | wfile = open(path.join(cpath, 'Fonts', 'fonts.txt'), mode='w', encoding='utf-8-sig')
1349 | maxnum = 0
1350 | for s in assfont.keys():
1351 | ssp = s.split('?')
1352 | if not ssp[0] == ssp[1]:
1353 | lx = len(assfont[s][0])
1354 | if lx > maxnum:
1355 | maxnum = lx
1356 | maxnum = len(str(maxnum))
1357 | for s in assfont.keys():
1358 | ssp = s.split('?')
1359 | if not ssp[0] == ssp[1]:
1360 | fp = ssp[0]
1361 | fn = path.basename(fp)
1362 | ann = ''
1363 | errshow = False
1364 | if copyfont:
1365 | try:
1366 | shutil.copy(fp, path.join(fd, fn))
1367 | ann = ' - copied'
1368 | except:
1369 | print('[ERROR]', sys.exc_info())
1370 | ann = ' - copy error'
1371 | errshow = True
1372 | if resultw:
1373 | print('{0} <{1}>{2}'.format(assfont[s][2], path.basename(fn), ann), file=wfile)
1374 | if errshow:
1375 | print('\033[1;32m[{3}]\033[0m \033[1;36m{0}\033[0m \033[1m<{1}>\033[1;31m{2}\033[0m'.format(assfont[s][2],
1376 | path.basename(fn), ann, str(len(assfont[s][0])).rjust(maxnum)))
1377 | else: print('\033[1;32m[{3}]\033[0m \033[1;36m{0}\033[0m \033[1m<{1}>\033[1;32m{2}\033[0m'.format(assfont[s][2],
1378 | path.basename(fn), ann, str(len(assfont[s][0])).rjust(maxnum)))
1379 | # print(assfont[s][0])
1380 | else:
1381 | if resultw:
1382 | print('{0} - No Found'.format(ssp[0]), file=wfile)
1383 | print('\033[1;31m[{1}]\033[1;36m {0}\033[1;31m - No Found\033[0m'.format(ssp[0], 'N'.center(maxnum, 'N')))
1384 | if resultw: wfile.close()
1385 | print('')
1386 | del assfont
1387 | del fontlist
1388 | elif work == 3:
1389 | if resultw: resultw = False
1390 | else: resultw = True
1391 | elif work == 4:
1392 | if copyfont: copyfont = False
1393 | else: copyfont = True
1394 | elif work == 5:
1395 | if s_subdir: s_subdir = False
1396 | else: s_subdir = True
1397 | else:
1398 | leave = False
1399 | if work < 3: os.system('pause')
1400 |
1401 |
1402 | def checkOutPath(op: str, default: str) -> str:
1403 | if op == '': return default
1404 | if op[0] == '?': return '?' + re.sub(cillegal, '_', op[1:])
1405 | if not path.isabs(op):
1406 | print('\033[1;31m[ERROR] 输入的必须是绝对路径或子目录名称!: \"{0}\"\033[0m'.format(op))
1407 | os.system('pause')
1408 | return default
1409 | if path.isdir(op): return op
1410 | if path.isfile(op): return path.dirname(op)
1411 | print('\033[1;31m[ERROR] 输入的必须是目录路径或子目录名称!: \"{0}\"\033[0m'.format(op))
1412 | os.system('pause')
1413 | return default
1414 |
1415 | def showMessageSubset(newasspaths: list, newfont_name: dict):
1416 | for ap in newasspaths:
1417 | if path.exists(ap):
1418 | print('\033[1;32m成功:\033[0m \033[1m\"{0}\"\033[0m'.format(path.basename(ap)))
1419 | for nf in newfont_name.keys():
1420 | if path.exists(newfont_name[nf][0]):
1421 | if newfont_name[nf][1] is None:
1422 | print('\033[1;31m失败:\033[1m \"{0}\"\033[0m >> \033[1m\"{1}\"\033[0m'.format(path.basename(nf),
1423 | path.basename(newfont_name[nf][0])))
1424 | else:
1425 | print('\033[1;32m成功:\033[1m \"{0}\"\033[0m >> \033[1m\"{1}\" ({2})\033[0m'.format(path.basename(nf),
1426 | path.basename(newfont_name[nf][0]), newfont_name[nf][1]))
1427 |
1428 | def cFontSubset(font_name):
1429 | global extlist, v_subdir, s_subdir, rmAssIn, rmAttach, \
1430 | mkvout, assout, fontout, matchStrict, no_mkvm, notfont
1431 | leave = True
1432 | while leave:
1433 | cls()
1434 | print('''ASFMKV & ASFMKV-FontSubset
1435 |
1436 | 选择功能:
1437 | [L] 回到上级菜单
1438 | [A] 子集化字体
1439 | [B] 子集化并封装
1440 |
1441 | 切换开关:
1442 | [1] 检视媒体扩展名列表 及 语言编码列表
1443 | [2] 搜索子目录(视频): \033[1;33m{0}\033[0m
1444 | [3] 搜索子目录(字幕): \033[1;33m{1}\033[0m
1445 | [4] (封装)移除内挂字幕: \033[1;33m{2}\033[0m
1446 | [5] (封装)移除原有附件: \033[1;33m{3}\033[0m
1447 | [6] (封装)不封装字体: \033[1;33m{8}\033[0m
1448 | [7] 严格字幕匹配: \033[1;33m{7}\033[0m
1449 | [8] 媒体文件输出文件夹: \033[1;33m{4}\033[0m
1450 | [9] 字幕文件输出文件夹: \033[1;33m{5}\033[0m
1451 | [0] 字体文件输出文件夹: \033[1;33m{6}\033[0m
1452 | '''.format(v_subdir, s_subdir, rmAssIn, rmAttach, mkvout, assout,
1453 | fontout, matchStrict, notfont))
1454 | work = 0
1455 | work = os.system('choice /M 请输入 /C AB1234567890L')
1456 | if work == 2 and no_mkvm:
1457 | print('[ERROR] 在您的系统中找不到 mkvmerge, 该功能不可用')
1458 | work = -1
1459 | if work in [1, 2]:
1460 | cls()
1461 | if work == 1: print('''子集化字体
1462 |
1463 | 搜索子目录(视频): \033[1;33m{0}\033[0m
1464 | 搜索子目录(字幕): \033[1;33m{1}\033[0m
1465 | 严格字幕匹配: \033[1;33m{2}\033[0m
1466 | 字幕文件输出文件夹: \033[1;33m{3}\033[0m
1467 | 字体文件输出文件夹: \033[1;33m{4}\033[0m
1468 | '''.format(v_subdir, s_subdir, matchStrict, assout, fontout))
1469 | else: print('''子集化字体并封装
1470 |
1471 | 搜索子目录(视频): \033[1;33m{4}\033[0m
1472 | 搜索子目录(字幕): \033[1;33m{5}\033[0m
1473 | 移除内挂字幕: \033[1;33m{0}\033[0m
1474 | 移除原有附件: \033[1;33m{1}\033[0m
1475 | 不封装字体: \033[1;33m{2}\033[0m
1476 | 严格字幕匹配: \033[1;33m{6}\033[0m
1477 | 媒体文件输出文件夹: \033[1;33m{3}\033[0m
1478 | '''.format(rmAssIn, rmAttach, notfont, mkvout, v_subdir, s_subdir, matchStrict))
1479 | cpath = ''
1480 | directout = False
1481 | subonly = False
1482 | subonlyp = ''
1483 | while not path.exists(cpath) and not directout:
1484 | directout = True
1485 | cpath = input('不输入任何值 直接回车回到上一页面\n请输入文件或目录路径: ').strip('\"')
1486 | if cpath == '': print('没有输入,回到上级菜单')
1487 | elif not path.isabs(cpath): print('\033[1;31m[ERROR] 输入的必须是绝对路径!: \"{0}\"\033[0m'.format(cpath))
1488 | elif not path.exists(cpath): print('\033[1;31m[ERROR] 找不到路径: \"{0}\"\033[0m'.format(cpath))
1489 | elif path.isfile(cpath):
1490 | testext = path.splitext(cpath)[1][1:].lower()
1491 | if testext in extlist:
1492 | directout = False
1493 | elif testext in ['ass', 'ssa'] and work == 1:
1494 | directout = False
1495 | subonly = True
1496 | else: print('\033[1;31m[ERROR] 扩展名不正确: \"{0}\"\033[0m'.format(cpath))
1497 | elif not path.isdir(cpath):
1498 | print('\033[1;31m[ERROR] 输入的应该是目录或媒体文件!: \"{0}\"\033[0m'.format(cpath))
1499 | else: directout = False
1500 | # print(directout)
1501 | if not directout:
1502 | medias = None
1503 | if path.isfile(cpath):
1504 | if not subonly: medias = [[path.splitext(path.basename(cpath))[0], cpath]]
1505 | else: subonlyp = cpath
1506 | cpath = path.dirname(cpath)
1507 | else: medias = getMediaFilelist(cpath)
1508 | # print(medias)
1509 |
1510 | if assout == '':
1511 | assout_cache = path.join(cpath, 'Subtitles')
1512 | elif assout[0] == '?':
1513 | assout_cache = path.join(cpath, assout[1:])
1514 | else: assout_cache = assout
1515 | if fontout == '':
1516 | fontout_cache = path.join(cpath, 'Fonts')
1517 | elif fontout[0] == '?':
1518 | fontout_cache = path.join(cpath, fontout[1:])
1519 | else: fontout_cache = fontout
1520 | if mkvout == '':
1521 | mkvout_cache = ''
1522 | elif mkvout[0] == '?':
1523 | mkvout_cache = path.join(cpath, mkvout[1:])
1524 | else: mkvout_cache = mkvout
1525 |
1526 | domux = False
1527 | if work == 2: domux = True
1528 |
1529 | if not medias is None:
1530 | if len(medias) > 0:
1531 | media_ass = getSubtitles(medias, cpath)
1532 | # print(media_ass)
1533 | for k in media_ass.keys():
1534 | #print(k)
1535 | #print([assout_cache, fontout_cache, mkvout_cache])
1536 | newasspaths, newfont_name, mkvr = main(font_name, media_ass[k],
1537 | mux = domux, outdir=[assout_cache, fontout_cache, mkvout_cache], vpath=k, asslangs=sublang)
1538 | if mkvr != -2:
1539 | showMessageSubset(newasspaths, newfont_name)
1540 | else:
1541 | break
1542 | elif subonly:
1543 | newasspaths, newfont_name, mkvr = main(font_name, [subonlyp],
1544 | mux = domux, outdir=[assout_cache, fontout_cache, mkvout_cache])
1545 | if mkvr != -2:
1546 | showMessageSubset(newasspaths, newfont_name)
1547 | elif work == 3:
1548 | cls()
1549 | print('ExtList Viewer 1.00-Final\n')
1550 | for i in range(0, len(extlist)):
1551 | s = extlist[i]
1552 | print('[Ext{0:>3d}] {1}'.format(i, s))
1553 | print('\n')
1554 | if not no_mkvm:
1555 | os.system('pause')
1556 | cls()
1557 | os.system('mkvmerge --list-languages')
1558 | #print('\033[1m mkvmerge 语言编码列表 \033[0m')
1559 | else:
1560 | print('没有检测到mkvmerge,无法输出语言编码列表')
1561 | elif work == 4:
1562 | if v_subdir: v_subdir = False
1563 | else: v_subdir = True
1564 | elif work == 5:
1565 | if s_subdir: s_subdir = False
1566 | else: s_subdir = True
1567 | elif work == 6:
1568 | if rmAssIn: rmAssIn = False
1569 | else: rmAssIn = True
1570 | elif work == 7:
1571 | if rmAttach: rmAttach = False
1572 | else: rmAttach = True
1573 | elif work == 8:
1574 | if notfont: notfont = False
1575 | else: notfont = True
1576 | elif work == 9:
1577 | if matchStrict: matchStrict = False
1578 | else: matchStrict = True
1579 | elif work in [10, 11, 12]:
1580 | cls()
1581 | print('''请输入目标目录路径或子目录名称\n(若要输入子目录,请在目录名前加\"?\")
1582 | (不支持多层子目录,会自动将\"\\\"换成下划线)''')
1583 | if work == 10:
1584 | mkvout = checkOutPath(input(), mkvout)
1585 | elif work == 11:
1586 | assout = checkOutPath(input(), assout)
1587 | elif work == 12:
1588 | fontout = checkOutPath(input(), fontout)
1589 | else:
1590 | leave = False
1591 | if work < 4:
1592 | os.system('pause')
1593 |
1594 | def cLicense():
1595 | cls()
1596 | print('''AddSubFontMKV Python Remake 1.01
1597 | Apache-2.0 License
1598 | Copyright(c) 2022 yyfll
1599 |
1600 | 依赖:
1601 | fontTools | MIT License
1602 | chardet | LGPL-2.1 License
1603 | colorama | BSD-3 License
1604 | mkvmerge | GPL-2 License
1605 | ''')
1606 | print('for more information:\nhttps://www.apache.org/licenses/')
1607 | os.system('pause')
1608 |
1609 | cls()
1610 |
1611 | if os.system('choice /? 1>nul 2>nul') > 0:
1612 | no_cmdc = True
1613 |
1614 | if __name__=="__main__":
1615 | while 1:
1616 | work = 0
1617 | print('''ASFMKV Python Remake 1.01 | (c) 2022 yyfll{0}
1618 | 字体名称数: [\033[1;33m{2}\033[0m](包含乱码的名称)
1619 |
1620 | 请选择功能:
1621 | [A] ListAssFont
1622 | [B] 字体子集化 & MKV封装
1623 | [C] 检视重复字体: 重复名称[\033[1;33m{1}\033[0m]
1624 |
1625 | 其他:
1626 | [D] 依赖与许可证
1627 | [L] 直接退出'''.format(mkvmv, len(dupfont.keys()), len(font_name.keys())))
1628 | print('')
1629 | work = os.system('choice /M 请选择: /C ABCDL')
1630 | if work == 1:
1631 | cListAssFont(font_name)
1632 | elif work == 2:
1633 | cFontSubset(font_name)
1634 | elif work == 3:
1635 | cls()
1636 | if len(dupfont.keys()) > 0:
1637 | for s in dupfont.keys():
1638 | print('\033[1;31m[{0}]\033[0m'.format(s))
1639 | for ss in dupfont[s]:
1640 | print('\"{0}\"'.format(ss))
1641 | print('')
1642 | else:
1643 | print('没有名称重复的字体')
1644 | os.system('pause')
1645 | elif work == 4:
1646 | cLicense()
1647 | elif work == 5:
1648 | exit()
1649 | else: exit()
1650 | cls()
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright [yyyy] [name of copyright owner]
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # AddSubFontMKV Python Remake (ASFMKV Py)
2 | **Copyright(c) 2022-2024 yyfll**
3 |
4 | **将您的字幕和字体通过mkvmerge快速批量封装到Matroska容器**
5 |
6 | **或是查看您的系统是否拥有字幕所需的字体**
7 |
8 | **亦或是将您的字体按照字幕进行子集化**
9 |
10 | 浪费时间打磨5年的ASFMKV批处理的正统后继者(X)
11 |
12 | 本脚本使用 Apache-2.0 许可证
13 | ## 关于Preview
14 | 由于部分功能是新开发的,目前仍存在多多少少的小问题
在本脚本最需要版本号蹭蹭蹭蹦的时候我个人非常忙,真是非常抱歉
15 |
16 | **尽管目前的最新版本仍然是Preview版本,但是它们都相对稳定而可靠**
17 |
18 | **您不应该使用过时的Preview16之前的任何版本,它们甚至不能正确分析ASS文件**
19 |
20 | ## 目录
21 | **新版本在Release,仓库还没得空整理暂时不放**
22 | | [最新更新](#CLI命令行版本更新) | [能干什么](#%E8%83%BD%E5%B9%B2%E4%BB%80%E4%B9%88) | [运行环境](#%E5%AE%89%E8%A3%85%E4%BE%9D%E8%B5%96%E7%BB%84%E4%BB%B6%E5%92%8C%E7%A8%8B%E5%BA%8F) | [功能介绍](#%E5%8A%9F%E8%83%BD%E4%BB%8B%E7%BB%8D) | [自定义变量](#%E8%87%AA%E5%AE%9A%E4%B9%89%E5%8F%98%E9%87%8F) | [字体信息缓存](#字体信息缓存) | [sublangs本地化](#sublangs本地化)
23 | | --- | --- | --- | --- | --- | --- | --- |
24 |
25 | ## 版本前瞻(画大饼)
26 | * PyQt Beta3版本
27 | * **重写 字体信息词典**
28 | * **添加命令行输入支持**
29 |
30 | ## CLI命令行版本更新
31 | ### Preview 19
32 | * 将子集化字体置入ASS/SSA作为附件
33 | * 支持抽出ASS/SSA中的字体附件
34 | * 删除了过时的功能以应对python 3.14
35 | * 修复一个小问题
36 |
37 | ## 能干什么?
38 | * 字体查重(借由**fontTools**实现,谢谢。)
39 | * ASS/SSA 依赖字体确认
40 | * 字体子集化(借由**fontTools**实现,谢谢。字体命名上参考了现有的子集化程序。)
41 | * 字幕字体批量封装到Matroska(借由**mkvmerge**实现,谢谢)
42 | * 字幕-视频文件名称匹配(**[RenameSubtitles_RE](https://github.com/DYY-Studio/RenameSubtitles_RE)** 1.1方法的改进版本)
43 | * ASS/SSA 批量转换为 UTF-8-BOM
44 | ## 安装依赖组件和程序
45 | #### [Python](https://www.python.org/downloads/windows/)
46 | #### 运行库
47 | * CLI版本: `pip3 install chardet fontTools colorama --upgrade`
48 | * GUI版本: `pip3 install chardet fontTools colorama PyQt5 --upgrade`
49 |
50 | #### [mkvmerge](https://mkvtoolnix.download/) (可选)
51 |
52 | 下载并安装/解压 MKVToolNix,将安装目录添加到系统变量path中
53 | #### [FFmpeg](https://ffmpeg.org/download.html)(Preview18+ 可选)
54 |
55 | 下载并解压 FFmpeg与FFprobe,将可执行文件所在目录添加到系统变量path中,或直接与ASFMKVpy放置在同一目录
56 |
57 | 推荐使用 FFmpeg 5.0 以上版本以避免兼容性问题
58 | ## 系统要求
59 | **Windows 7 SP1 专业版及以上,python 3.7 及以上,cmd 必须支持 choice 命令**
60 |
61 | ** 不支持 Linux **
62 | #### 测试环境
63 | * Windows10 21H2、21H1、20H1(2004) 及 Windows7 SP1 (已打全部补丁)
64 | * Python 3.9.5 / 3.8.10
65 | * mkvmerge v48.0.0 / v65.0.0
66 | * FFmpeg v5.1.2
67 |
68 | # 功能介绍
69 | | K | 功能 | K | 功能 |
70 | |---------|---------|---------|---------|
71 | | A | [列出所需字体](#列出所需字体) | W | [检视旧格式字体](#检视旧格式字体) |
72 | | B | [字体子集化 & MKV封装](#字体子集化及封装)
[**sublangs教程(Preview17+)**](#sublangs教程) | U | [字幕批量转换UTF-8-BOM](#字幕文本编码批量转换) |
73 | | M | [字幕-视频名称匹配](#字幕-视频名称匹配) | C | [检视重复名称字体](#检视重复名称字体) |
74 | | S | [搜索字体(Debug)](#搜索字体) | R | [字体信息重载](#字体信息重载) |
75 |
76 | 启动时需要先扫描注册表里的字体和用户给的自定义字体目录(如果有)
77 | ## 列出所需字体
78 | (名称来源于前辈的软件 **ListAssFont**,这名字很精炼,我找不到词代替它)
79 |
80 | (这一部分的输出格式很大程度上参考了前辈的设计,谢谢!)
81 |
82 | 为了更快的找到字幕中所述字体名对应的字体,ASFMKV采用了以字体名称作为Key的词典
83 |
84 | 因而这里的格式与前辈的 ListAssFont 不同
85 | ### [A] 列出全部字体
86 | 会输出 字体文件名(无路径) 与 字体名称 的对应关系
87 | ### [B] 检查并列出
88 | 需要用户输入ASS/SSA字幕文件的路径或其所在目录
89 |
90 | 会输出 字幕文件内规定的字体名称 与 字体文件名 的对应关系
91 | ## 字体子集化及封装
92 | ASFMKV的传统功能以及字体子集化功能
93 |
94 | **请参见[字体黑名单](https://github.com/DYY-Studio/AddSubFontMKV_py/wiki/%E5%AD%97%E4%BD%93%E9%BB%91%E5%90%8D%E5%8D%95-%E4%B9%B1%E7%A0%81---Fonts-Blacklist---Decode-Error)以减少子集化错误的可能性**
95 | ### [A] 子集化字体
96 | 需要用户输入有外挂字幕的视频文件的路径或其所在目录
97 |
98 | 会创建子文件夹 `subs` 和 `Fonts`(如果用户没有规定)输出子集化完成的字幕和字体
99 |
100 | 字体会在 `Fonts` 下的以各视频文件匹配到的第一个字幕的文件名作为名称文件夹下
101 |
102 | 字幕会带有`.subset`标注
103 | ### [B/C/D] 子集化并封装
104 | 需要用户输入有外挂字幕的视频文件的路径或其所在目录
105 |
106 | 会自动完成封装流程,并删除子集化的字体和修改过的字幕
107 |
108 | 默认情况下输出到源文件夹,文件名加有 `.muxed`
109 |
110 | **也可以把 notfont 设置为 True,只封装字幕,不封装字体,不进行子集化**
111 |
112 | #### ASS/SSA内嵌
113 | 使用ASS/SSA的“附件”功能将字体内嵌于ASS文件`[Fonts]`。
114 |
115 | | 字幕滤镜 | 兼容 | 播放器内置 | 兼容 |
116 | | -- | :-: | -- | :-: |
117 | | libass | ✅ | PotPlayer | ✅ |
118 | | XySubFilter | ❌ | MPC-HC/BE | ✅ |
119 | | VSFilterMod | ✅ | mpv | ✅ |
120 | | xy-VSFilter | ✅ | - | - |
121 | | VSFilter | ✅ | nPlayer | ✅ |
122 |
123 |
124 | #### mkvmerge
125 |
126 | 传统的使用mkvmerge封装,经过大量测试,比较稳定
127 |
128 | 需要 `mkvmerge.exe` 在`path`系统变量中的路径下或与ASFMKVpy在同一目录
129 | #### FFmpeg
130 |
131 | **Preview 18**新增的使用ffmpeg进行MKV封装,测试较少,仅保证部分情况正常使用
132 |
133 | 默认用于没有mkvmerge的环境,并且由于ffmpeg的元数据写入方式,同时需要ffprobe支持
134 |
135 | 需要您的ffmpeg版本较新,支持`-attach`,建议使用5.0以上版本
136 |
137 | 需要 `ffmpeg.exe`和`ffprobe.exe` 在`path`系统变量中的路径下或与ASFMKVpy在同一目录
138 |
139 | ### sublangs教程
140 | 在 Preview 17 中,我加入了拖了大半年的sublangs询问输入功能,在用户交互上废了很多心思
141 |
142 | 但是使用起来还是比较麻烦,所以在这里特别讲一下
143 |
144 | * *演示用视频文件: [SFEO-Raws] ACCA13区监察课 ACCA 13-ku Kansatsu-ka 01-12+SP (BD 720P x264 10bit AAC)*
145 | * *演示用字幕文件: 动漫国字幕组 ACCA13区监察课*
146 |
147 | 1. 当你在Preview17+使用 **子集化字体并封装** 功能时,输入路径回车后,会询问您是否要为字幕轨道添加语言信息
148 | 
149 | 2. 选择`Y`后,程序需要从mkvmerge命令行获取ISO-639语言列表,由于语言较多,可能比较耗时
150 | 3. 然后您就进入了`sublangs`输入界面

151 | 示例字幕是您输入的目录中,**外挂字幕最多的视频文件**对应的外挂字幕
152 | 在该界面,高亮的示例字幕代表您目前输入的语言代码所**要应用到的字幕**
其它视频文件的外挂字幕,也将按照这个名称顺序应用,**请避免在使用该功能时混用名称排序不同的字幕**
153 | 4. 您可以输入 ISO-639-1/2/3 语言代码,也可以输入该语言对应的英文名称(mkvmerge的语言列表没有其它语言支持)
154 |
如要输入中文,直接输入ISO-639-3 `chi`,或ISO-639-2 `zh`
155 |
也可以可以搜索`Chinese`
156 |

157 |
然后选择`4`
158 |

159 | 5. 当您确认了输入的语言后,在刚刚的字幕示例的末尾,会显示您选择的语言的英文名称,程序则会向您请求下一个字幕的语言
160 |

161 | 6. 在全部字幕示例的语言代码输入完毕后,程序会向您展示全部字幕的语言代码应用情况
162 |
如果您认为之前的输入有误,您可以在这里输入`R`,如果您突然不想用语言代码了,可以输入`C`,否则输入`Y`继续封装
163 |
164 | 附上一些常用的ISO-639-3编码对应
165 | | 语言 | ISO-639-3 | 语言 | ISO-639-3
166 | | --- | --- | --- | --- |
167 | | 中文 | chi | 未知 | und |
168 | | 日文 | jpn | 韩文 | kor |
169 | | 英文 | eng | 法语 | fre |
170 |
171 | 您可以使用 `mkvmerge --list-languages` 来查看全部语言
172 |
173 | ## 字幕-视频名称匹配
174 | 本功能是 **[RenameSubtitles_RE](https://github.com/DYY-Studio/RenameSubtitles_RE)** 项目1.1方法的python实现
175 |
176 | 通过简单运算得到视频文件名和字幕文件名中剧集数所在位置,然后直接套用,具体算法参见隔壁项目页
177 |
178 | 功能特色
179 | * 非常快
180 | * 有一定的准确度
181 | * 支持多字幕
182 | * 支持自动字幕后缀信息,或者手动输入多字幕后缀信息
183 |
184 | 本功能需要您的字幕文件和视频文件使用相同的集数表达,`5.1`和`5.5`,`OVA01`和`OVA1`是无法匹配的
185 |
186 | ## 搜索字体
187 | DEBUG用的功能,可以在程序读取的所有字体信息中搜索字体
188 | ### 完全匹配
189 | 就是完全匹配,只有输入的名称与字体信息中的名称完全相符才会显示
190 | ### 部分匹配
191 | 只支持` `(半角空格)作为"与(AND)"操作符
192 |
193 | 在字体名称中有对应关键词就会输出
194 |
195 | ## 检视重复名称字体
196 | 列出同一字体名称下的字体文件
197 |
198 | 注意:必须满足以下条件才会显示在这里
199 | 1. 文件名不相同(包括扩展名)
200 | 2. 必须是从注册表读取的系统已安装字体
201 |
202 | ## 检视旧格式字体
203 | 在fontTools对字体的读取与子集化进行下一步的修正前,这里会列出与fontTools不兼容的旧格式字体
204 |
205 | 这些字体很有可能不能正常的被子集化,您可能需要对这些字体使用字体编辑软件重新保存来兼容fontTools
206 |
207 | ## 字幕文本编码批量转换
208 | 就是单纯的把你的 `.ass` `.ssa` `.srt`文件转为UTF-8-BOM以便其它应用使用,基于chardet编码探测
209 |
210 | ## 字体信息重载
211 | ### 1 更新缓存
212 | 以标准启动方式重新加载字体信息
213 | ### 2 仅覆盖当前使用的缓存文件并重载
214 | 完全重建当前使用的字体信息缓存
215 | ### 3 删除所有缓存文件并重载
216 | 把现在没使用的字体信息缓存也删了
217 | ### 4 新增自定义字体目录并重载
218 | 允许用户添加多个自定义字体目录
219 |
220 | # 高级
221 | ### 崩溃反馈
222 | 1. 打开 命令提示行/cmd ,输入`py "脚本绝对路径"`,重复您崩溃前所做的操作
或 使用有断点调试功能的编辑器(如VS Code),对该脚本进行调试,重复您崩溃前所做的操作
223 |
224 | 2. 发送崩溃信息 及 致使崩溃的操作 到作者邮箱,或在GitHub提出issue
225 | ### 自定义变量
226 | 自定义变量是沿袭自ASFMKV批处理版本的高级自定义操作
227 |
228 | 在Python Remake中,大部分自定义变量都可以在运行过程中快捷更改
229 |
230 | 如果您想要更改这些变量的默认值,或是更改部分无法在运行中更改的变量,请往下看
231 | #### 变更自定义变量
232 | 1. 使用编辑器打开本脚本
(注意,您的编辑器必须支持 UTF-8 NoBOM,不建议Win7用户使用记事本打开)
233 | 2. 在`import`部分的下方就是本脚本的自定义变量
编辑时请注意语法,不要移除单引号,绝对路径需要添加r在左侧引号前,右侧引号前不应有反斜杠
234 | 3. 保存
235 | #### 运行时不能变更的自定义变量
236 | | 变量名 | 作用 |
237 | | --- | --- |
238 | | extlist | 指定视频文件的扩展名 |
239 | | fontin | 外部字体文件夹的路径 |
240 | | sublang | 给字幕轨道赋予语言编码 |
241 | | o_fontload | 禁用注册表字体库和自定义字体库 |
242 | | s_fontload | 加载工作目录的子目录中的字体 |
243 | | f_priority | 控制各字体源的优先顺序 |
244 | | no_extcheck | 关闭mkvmerge兼容扩展名检查 |
245 | # 字体信息缓存
246 | 该部分将介绍 Preview 17 版本中字体信息缓存的实现方式
247 |
248 | 使用JSON配置实在是太简单了,所以缓存以JSON形式保存,在程序中则保存为一个词典
249 |
250 | 缓存的JSON保存在`%APPDATA%/ASFMKVpy`下
251 | ### 目录索引
252 | Preview 17 字体信息缓存采用目录形式缓存,将同一目录(不包括子目录)下的字体信息包含在一个JSON文件中
253 |
254 | JSON文件名称即为索引,是 **目录绝对路径(转小写)再使用UTF-8编码** 的 **CRC32** 校验值
255 |
256 | 因此缓存采用随用随读的形式,程序校验文件所在目录路径再寻找对应的缓存文件。
257 |
258 | ### 字体索引
259 | 为了防止字体变更导致信息失效,字体索引是由三个成分构成一段**CRC32校验值**
260 |
261 | **字体文件绝对路径(转小写) + 字体文件大小(Byte) + 文件变更时间(Float) 再使用 UTF-8 编码**
262 |
263 | # sublangs本地化
264 | 从 **Preview 18** 版本开始,sublangs选择器支持ISO-639语言的本地化名称读取
265 |
266 | 您需要将本地化文件放入 `%APPDATA%\ASFMKVpy\isoLang\对应语言的ISO639-1语言代码`
267 |
268 | 如简体中文应将本地化文件放入 `%APPDATA%\ASFMKVpy\isoLang\zh_CN`
269 |
270 | 程序在启动时,会通过即将过时的`locale.getdefaultlocale()`获取当前系统所用语言的ISO639-1语言代码
271 |
272 | 在执行sublangs选择器时,程序会扫描上述文件夹寻找本地语言对应的本地化文件,如果没有子集(如zh_CN),则寻找同一语言下的其它子集(zh、zh_Hans...)
273 |
274 | 每次在Release时我都会提供一个全语言本地化压缩包,请根据自己所在地语言选择放入
275 |
276 | **提供的本地化语言包是 [language-list](https://github.com/umpirsky/language-list) 项目和 [pycountry](https://github.com/flyingcircusio/pycountry) 项目的混合**
277 | **前者提供了ISO-639-1对应的翻译文本,而后者提供了ISO-639-2T/2B/3中对应的语言代码**
278 | **由衷地感谢这两个项目**
279 |
280 | ## 自定义本地化文件
281 |
282 | 本地化JSON应遵循以下语法:
283 | `{"[ISO-639-1/2B/2T/3]": "[对应的本地化名称]"}`
284 |
285 | # 未测试功能
286 | #### mkvout, fontout, assout
287 | 设置为绝对路径的情况暂未测试
288 | # 缺点
289 | ### 程序结构复杂
290 | 写到后面逻辑乱了,很多可以函数化的东西被反复用了
291 | ### 错误易崩溃
292 | 对错误的预防只有最低限度,一旦错了就炸了,没有Module_DEBUG的第一天……
293 | ### 注释不够多
294 | 自己读起来都痛苦
295 | # 致谢
296 | ### [fontTools](https://github.com/fonttools/fonttools) (MIT Licence)
297 | ### [MKVToolNix/mkvmerge](https://mkvtoolnix.download/) (GPLv2 Licence)
298 | ### [chardet](https://github.com/chardet/chardet) (LGPLv2.1 Licence)
299 | ### [colorama](https://github.com/tartley/colorama) (BSD-3-Clause License)
300 | ### [language-list](https://github.com/umpirsky/language-list) (MIT License)
301 | ### [pycountry](https://github.com/flyingcircusio/pycountry) (LGPLv2.1 License)
302 |
--------------------------------------------------------------------------------