├── 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 | ![图片](https://github.com/DYY-Studio/AddSubFontMKV_py/assets/48157880/37558f01-038c-40c3-ade7-3f9c6867fc7c) 149 | 2. 选择`Y`后,程序需要从mkvmerge命令行获取ISO-639语言列表,由于语言较多,可能比较耗时 150 | 3. 然后您就进入了`sublangs`输入界面
![图片](https://github.com/DYY-Studio/AddSubFontMKV_py/assets/48157880/6429dc2e-9ad1-4aa1-a1e1-dbce4d821c56)
151 | 示例字幕是您输入的目录中,**外挂字幕最多的视频文件**对应的外挂字幕
152 | 在该界面,高亮的示例字幕代表您目前输入的语言代码所**要应用到的字幕**
其它视频文件的外挂字幕,也将按照这个名称顺序应用,**请避免在使用该功能时混用名称排序不同的字幕** 153 | 4. 您可以输入 ISO-639-1/2/3 语言代码,也可以输入该语言对应的英文名称(mkvmerge的语言列表没有其它语言支持) 154 |
如要输入中文,直接输入ISO-639-3 `chi`,或ISO-639-2 `zh` 155 |
也可以可以搜索`Chinese` 156 |
![图片](https://github.com/DYY-Studio/AddSubFontMKV_py/assets/48157880/bc99406b-94bc-4104-a353-cb18e5a9a964) 157 |
然后选择`4` 158 |
![图片](https://github.com/DYY-Studio/AddSubFontMKV_py/assets/48157880/59767022-e1a0-4606-a8ab-76c0eefba8c0) 159 | 5. 当您确认了输入的语言后,在刚刚的字幕示例的末尾,会显示您选择的语言的英文名称,程序则会向您请求下一个字幕的语言 160 |
![图片](https://github.com/DYY-Studio/AddSubFontMKV_py/assets/48157880/99be82a4-7384-458b-9b91-59e8fe0cd37c) 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 | --------------------------------------------------------------------------------