easyrip 3.13.2__py3-none-any.whl → 4.9.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- easyrip/__init__.py +5 -1
- easyrip/__main__.py +124 -15
- easyrip/easyrip_command.py +457 -148
- easyrip/easyrip_config/config.py +269 -0
- easyrip/easyrip_config/config_key.py +28 -0
- easyrip/easyrip_log.py +120 -42
- easyrip/easyrip_main.py +509 -259
- easyrip/easyrip_mlang/__init__.py +20 -45
- easyrip/easyrip_mlang/global_lang_val.py +18 -16
- easyrip/easyrip_mlang/lang_en.py +1 -1
- easyrip/easyrip_mlang/lang_zh_Hans_CN.py +101 -77
- easyrip/easyrip_mlang/translator.py +12 -10
- easyrip/easyrip_prompt.py +73 -0
- easyrip/easyrip_web/__init__.py +2 -1
- easyrip/easyrip_web/http_server.py +56 -42
- easyrip/easyrip_web/third_party_api.py +60 -8
- easyrip/global_val.py +21 -1
- easyrip/ripper/media_info.py +10 -3
- easyrip/ripper/param.py +482 -0
- easyrip/ripper/ripper.py +260 -574
- easyrip/ripper/sub_and_font/__init__.py +10 -0
- easyrip/ripper/{font_subset → sub_and_font}/ass.py +95 -84
- easyrip/ripper/{font_subset → sub_and_font}/font.py +72 -79
- easyrip/ripper/{font_subset → sub_and_font}/subset.py +122 -81
- easyrip/utils.py +129 -27
- easyrip-4.9.1.dist-info/METADATA +92 -0
- easyrip-4.9.1.dist-info/RECORD +31 -0
- easyrip/easyrip_config.py +0 -198
- easyrip/ripper/__init__.py +0 -10
- easyrip/ripper/font_subset/__init__.py +0 -7
- easyrip-3.13.2.dist-info/METADATA +0 -89
- easyrip-3.13.2.dist-info/RECORD +0 -29
- {easyrip-3.13.2.dist-info → easyrip-4.9.1.dist-info}/WHEEL +0 -0
- {easyrip-3.13.2.dist-info → easyrip-4.9.1.dist-info}/entry_points.txt +0 -0
- {easyrip-3.13.2.dist-info → easyrip-4.9.1.dist-info}/licenses/LICENSE +0 -0
- {easyrip-3.13.2.dist-info → easyrip-4.9.1.dist-info}/top_level.txt +0 -0
|
@@ -5,6 +5,7 @@ from dataclasses import dataclass, field
|
|
|
5
5
|
from pathlib import Path
|
|
6
6
|
|
|
7
7
|
from ...easyrip_log import log
|
|
8
|
+
from ...easyrip_mlang import Mlang_exception
|
|
8
9
|
from ...utils import read_text, uudecode_ssa, uuencode_ssa
|
|
9
10
|
|
|
10
11
|
|
|
@@ -39,13 +40,13 @@ class Style_fmt_it(enum.Enum):
|
|
|
39
40
|
# Data = "Dialogue"
|
|
40
41
|
|
|
41
42
|
|
|
42
|
-
@dataclass(slots=True)
|
|
43
|
+
@dataclass(slots=True, kw_only=True)
|
|
43
44
|
class Script_info_data:
|
|
44
45
|
# type: Script_info_type
|
|
45
46
|
raw_str: str
|
|
46
47
|
|
|
47
48
|
|
|
48
|
-
@dataclass(slots=True)
|
|
49
|
+
@dataclass(slots=True, kw_only=True)
|
|
49
50
|
class Script_info:
|
|
50
51
|
data: list[Script_info_data] = field(default_factory=list[Script_info_data])
|
|
51
52
|
|
|
@@ -58,7 +59,7 @@ class Script_info:
|
|
|
58
59
|
)
|
|
59
60
|
|
|
60
61
|
|
|
61
|
-
@dataclass(slots=True)
|
|
62
|
+
@dataclass(slots=True, kw_only=True)
|
|
62
63
|
class Style_data:
|
|
63
64
|
Name: str
|
|
64
65
|
Fontname: str
|
|
@@ -112,7 +113,7 @@ DEFAULT_STYLE_FMT_ORDER = (
|
|
|
112
113
|
)
|
|
113
114
|
|
|
114
115
|
|
|
115
|
-
@dataclass(slots=True)
|
|
116
|
+
@dataclass(slots=True, kw_only=True)
|
|
116
117
|
class Styles:
|
|
117
118
|
fmt_order: tuple[
|
|
118
119
|
Style_fmt_it,
|
|
@@ -168,14 +169,14 @@ class Styles:
|
|
|
168
169
|
)
|
|
169
170
|
data: list[Style_data] = field(default_factory=list[Style_data])
|
|
170
171
|
|
|
171
|
-
def flush_fmt_order_index(self):
|
|
172
|
+
def flush_fmt_order_index(self) -> None:
|
|
172
173
|
for it in self.fmt_order:
|
|
173
174
|
if index := next(
|
|
174
175
|
(i for i, v in enumerate(DEFAULT_STYLE_FMT_ORDER) if v == it), None
|
|
175
176
|
):
|
|
176
177
|
self.fmt_index[it] = index
|
|
177
178
|
else:
|
|
178
|
-
raise
|
|
179
|
+
raise Ass_generate_error(
|
|
179
180
|
f"Style Format flush order index err: can not find {it}"
|
|
180
181
|
)
|
|
181
182
|
|
|
@@ -236,7 +237,7 @@ class Styles:
|
|
|
236
237
|
Encoding=int(style_tuple[self.fmt_index[Style_fmt_it.Encoding]]),
|
|
237
238
|
)
|
|
238
239
|
except ValueError as e:
|
|
239
|
-
raise
|
|
240
|
+
raise Ass_generate_error from e
|
|
240
241
|
|
|
241
242
|
return res
|
|
242
243
|
|
|
@@ -248,16 +249,14 @@ class Styles:
|
|
|
248
249
|
*(
|
|
249
250
|
"Style: "
|
|
250
251
|
+ ",".join(
|
|
251
|
-
(
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
else v
|
|
258
|
-
)
|
|
259
|
-
for k in self.fmt_order
|
|
252
|
+
str(
|
|
253
|
+
v
|
|
254
|
+
if type(v := getattr(style, k.value)) is not float
|
|
255
|
+
else int(v)
|
|
256
|
+
if v == int(v)
|
|
257
|
+
else v
|
|
260
258
|
)
|
|
259
|
+
for k in self.fmt_order
|
|
261
260
|
)
|
|
262
261
|
for style in self.data
|
|
263
262
|
),
|
|
@@ -277,13 +276,17 @@ class Event_fmt_it(enum.Enum):
|
|
|
277
276
|
Effect = "Effect"
|
|
278
277
|
Text = "Text"
|
|
279
278
|
|
|
279
|
+
# 兼容旧版和不规范格式
|
|
280
|
+
Marked = Layer
|
|
281
|
+
Actor = Name
|
|
282
|
+
|
|
280
283
|
|
|
281
284
|
class Event_type(enum.Enum):
|
|
282
285
|
Dialogue = "Dialogue"
|
|
283
286
|
Comment = "Comment"
|
|
284
287
|
|
|
285
288
|
|
|
286
|
-
@dataclass(slots=True)
|
|
289
|
+
@dataclass(slots=True, kw_only=True)
|
|
287
290
|
class Event_data:
|
|
288
291
|
type: Event_type
|
|
289
292
|
|
|
@@ -300,69 +303,40 @@ class Event_data:
|
|
|
300
303
|
|
|
301
304
|
@staticmethod
|
|
302
305
|
def parse_text(text: str, use_libass_spec: bool) -> list[tuple[bool, str]]:
|
|
303
|
-
|
|
304
|
-
# 模式1: 不处理转义字符
|
|
305
|
-
result: list[tuple[bool, str]] = []
|
|
306
|
-
current = list[str]() # 当前累积的字符
|
|
307
|
-
in_tag = False # 是否在标签内
|
|
308
|
-
|
|
309
|
-
for char in text:
|
|
310
|
-
if in_tag is False:
|
|
311
|
-
if char == "{":
|
|
312
|
-
# 开始新标签,先保存当前累积的普通文本
|
|
313
|
-
if current:
|
|
314
|
-
result.append((False, "".join(current)))
|
|
315
|
-
current = []
|
|
316
|
-
current.append(char)
|
|
317
|
-
in_tag = True
|
|
318
|
-
else:
|
|
319
|
-
current.append(char) # 普通文本
|
|
320
|
-
else:
|
|
321
|
-
current.append(char) # 标签内容
|
|
322
|
-
if char == "}":
|
|
323
|
-
# 标签结束
|
|
324
|
-
result.append((True, "".join(current)))
|
|
325
|
-
current = []
|
|
326
|
-
in_tag = False
|
|
306
|
+
result: list[tuple[bool, str]] = []
|
|
327
307
|
|
|
328
|
-
|
|
329
|
-
if current:
|
|
330
|
-
result.append((False, "".join(current)))
|
|
331
|
-
return result
|
|
332
|
-
|
|
333
|
-
else:
|
|
308
|
+
if use_libass_spec:
|
|
334
309
|
# 模式2: 处理转义字符(libass规范)
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
escape_next = False # 下一个字符是否转义
|
|
310
|
+
current: list[str] = [] # 当前累积的字符
|
|
311
|
+
is_in_tag = False # 是否在标签内
|
|
312
|
+
is_escape_next = False # 下一个字符是否转义
|
|
339
313
|
|
|
340
314
|
for char in text:
|
|
341
|
-
if
|
|
315
|
+
if is_escape_next:
|
|
342
316
|
# 处理转义字符(任何字符直接作为普通字符)
|
|
343
317
|
current.append(char)
|
|
344
|
-
|
|
318
|
+
is_escape_next = False
|
|
345
319
|
elif char == "\\":
|
|
346
320
|
# 标记下一个字符为转义
|
|
347
321
|
current.append(char)
|
|
348
|
-
|
|
322
|
+
is_escape_next = True
|
|
349
323
|
elif char == "{":
|
|
350
|
-
if not
|
|
324
|
+
if not is_in_tag:
|
|
351
325
|
# 开始新标签(非转义的{)
|
|
352
326
|
if current:
|
|
353
327
|
result.append((False, "".join(current)))
|
|
354
328
|
current = []
|
|
355
329
|
current.append(char)
|
|
356
|
-
|
|
330
|
+
is_in_tag = True
|
|
357
331
|
else:
|
|
358
332
|
current.append(char) # 标签内的{
|
|
359
333
|
elif char == "}":
|
|
360
|
-
if
|
|
334
|
+
if is_in_tag:
|
|
361
335
|
# 非转义的}结束标签
|
|
362
336
|
current.append(char)
|
|
363
337
|
result.append((True, "".join(current)))
|
|
364
338
|
current = []
|
|
365
|
-
|
|
339
|
+
is_in_tag = False
|
|
366
340
|
else:
|
|
367
341
|
current.append(char) # 普通文本的}
|
|
368
342
|
else:
|
|
@@ -373,6 +347,34 @@ class Event_data:
|
|
|
373
347
|
result.append((False, "".join(current)))
|
|
374
348
|
return result
|
|
375
349
|
|
|
350
|
+
# 模式1: 不处理转义字符
|
|
351
|
+
current: list[str] = [] # 当前累积的字符
|
|
352
|
+
is_in_tag = False # 是否在标签内
|
|
353
|
+
|
|
354
|
+
for char in text:
|
|
355
|
+
if is_in_tag is False:
|
|
356
|
+
if char == "{":
|
|
357
|
+
# 开始新标签,先保存当前累积的普通文本
|
|
358
|
+
if current:
|
|
359
|
+
result.append((False, "".join(current)))
|
|
360
|
+
current = []
|
|
361
|
+
current.append(char)
|
|
362
|
+
is_in_tag = True
|
|
363
|
+
else:
|
|
364
|
+
current.append(char) # 普通文本
|
|
365
|
+
else:
|
|
366
|
+
current.append(char) # 标签内容
|
|
367
|
+
if char == "}":
|
|
368
|
+
# 标签结束
|
|
369
|
+
result.append((True, "".join(current)))
|
|
370
|
+
current = []
|
|
371
|
+
is_in_tag = False
|
|
372
|
+
|
|
373
|
+
# 处理剩余部分
|
|
374
|
+
if current:
|
|
375
|
+
result.append((False, "".join(current)))
|
|
376
|
+
return result
|
|
377
|
+
|
|
376
378
|
|
|
377
379
|
DEFAULT_EVENT_FMT_ORDER = (
|
|
378
380
|
Event_fmt_it.Layer,
|
|
@@ -388,7 +390,7 @@ DEFAULT_EVENT_FMT_ORDER = (
|
|
|
388
390
|
)
|
|
389
391
|
|
|
390
392
|
|
|
391
|
-
@dataclass(slots=True)
|
|
393
|
+
@dataclass(slots=True, kw_only=True)
|
|
392
394
|
class Events:
|
|
393
395
|
fmt_order: tuple[
|
|
394
396
|
Event_fmt_it,
|
|
@@ -418,14 +420,14 @@ class Events:
|
|
|
418
420
|
)
|
|
419
421
|
data: list[Event_data] = field(default_factory=list[Event_data])
|
|
420
422
|
|
|
421
|
-
def flush_fmt_order_index(self):
|
|
423
|
+
def flush_fmt_order_index(self) -> None:
|
|
422
424
|
for it in self.fmt_order:
|
|
423
425
|
if index := next(
|
|
424
426
|
(i for i, v in enumerate(DEFAULT_EVENT_FMT_ORDER) if v == it), None
|
|
425
427
|
):
|
|
426
428
|
self.fmt_index[it] = index
|
|
427
429
|
else:
|
|
428
|
-
raise
|
|
430
|
+
raise Ass_generate_error(
|
|
429
431
|
f"Event Format flush order index err: can not find {it}"
|
|
430
432
|
)
|
|
431
433
|
|
|
@@ -460,7 +462,7 @@ class Events:
|
|
|
460
462
|
Text=event_tuple[self.fmt_index[Event_fmt_it.Text]],
|
|
461
463
|
)
|
|
462
464
|
except ValueError as e:
|
|
463
|
-
raise
|
|
465
|
+
raise Ass_generate_error from e
|
|
464
466
|
|
|
465
467
|
return res
|
|
466
468
|
|
|
@@ -505,7 +507,7 @@ class Attach_type(enum.Enum):
|
|
|
505
507
|
Graphics = "Graphics"
|
|
506
508
|
|
|
507
509
|
|
|
508
|
-
@dataclass(slots=True)
|
|
510
|
+
@dataclass(slots=True, kw_only=True)
|
|
509
511
|
class Attachment_data:
|
|
510
512
|
type: Attach_type
|
|
511
513
|
name: str
|
|
@@ -536,7 +538,7 @@ class Attachment_data:
|
|
|
536
538
|
self.org_data = new_data
|
|
537
539
|
|
|
538
540
|
|
|
539
|
-
@dataclass(slots=True)
|
|
541
|
+
@dataclass(slots=True, kw_only=True)
|
|
540
542
|
class Attachments:
|
|
541
543
|
data: list[Attachment_data] = field(default_factory=list[Attachment_data])
|
|
542
544
|
|
|
@@ -569,7 +571,7 @@ class Attachments:
|
|
|
569
571
|
return res[1:] if len(res) else "" # 去除头部的 \n
|
|
570
572
|
|
|
571
573
|
|
|
572
|
-
@dataclass(slots=True)
|
|
574
|
+
@dataclass(slots=True, kw_only=True)
|
|
573
575
|
class Unknown_data:
|
|
574
576
|
head: str
|
|
575
577
|
data: list[str] = field(default_factory=list[str])
|
|
@@ -583,15 +585,15 @@ class Unknown_data:
|
|
|
583
585
|
)
|
|
584
586
|
|
|
585
587
|
|
|
586
|
-
class
|
|
588
|
+
class Ass_generate_error(Mlang_exception):
|
|
587
589
|
pass
|
|
588
590
|
|
|
589
591
|
|
|
590
592
|
class Ass:
|
|
591
|
-
def __init__(self, path: str | Path):
|
|
593
|
+
def __init__(self, path: str | Path) -> None:
|
|
592
594
|
path = Path(path)
|
|
593
595
|
if not path.is_file():
|
|
594
|
-
|
|
596
|
+
raise Ass_generate_error("Not a file: {}", path)
|
|
595
597
|
|
|
596
598
|
self.script_info: Script_info = Script_info()
|
|
597
599
|
self.styles: Styles = Styles()
|
|
@@ -630,22 +632,34 @@ class Ass:
|
|
|
630
632
|
case _:
|
|
631
633
|
if bool(re.search(r"[a-z]", head)):
|
|
632
634
|
state = State.unknown
|
|
633
|
-
new_unknown_data = Unknown_data(head)
|
|
635
|
+
new_unknown_data = Unknown_data(head=head)
|
|
634
636
|
|
|
635
637
|
elif line.startswith("Format:"):
|
|
636
|
-
|
|
638
|
+
formats_tuple = tuple(map(str.strip, line[7:].split(",")))
|
|
637
639
|
match state:
|
|
638
640
|
case State.styles:
|
|
639
|
-
format_order = tuple(Style_fmt_it
|
|
641
|
+
format_order = tuple(map(Style_fmt_it, formats_tuple))
|
|
640
642
|
if len(format_order) != 23:
|
|
641
|
-
raise
|
|
643
|
+
raise Ass_generate_error("Style Format len != 23")
|
|
642
644
|
|
|
643
645
|
self.styles.fmt_order = format_order
|
|
644
646
|
|
|
645
647
|
case State.events:
|
|
646
|
-
|
|
648
|
+
try:
|
|
649
|
+
format_order = tuple(
|
|
650
|
+
map(Event_fmt_it.__getitem__, formats_tuple)
|
|
651
|
+
)
|
|
652
|
+
except ValueError as e:
|
|
653
|
+
raise Ass_generate_error from e
|
|
654
|
+
|
|
647
655
|
if len(format_order) != 10:
|
|
648
|
-
raise
|
|
656
|
+
raise Ass_generate_error("Event Format len != 10")
|
|
657
|
+
|
|
658
|
+
if "Marked" in formats_tuple:
|
|
659
|
+
log.error(
|
|
660
|
+
"The ASS Events Format version too old: {}",
|
|
661
|
+
"It used 'Marked' instead of 'Layer'. 'Marked' has been replaced with 'Layer', which will result in irreversible info loss",
|
|
662
|
+
)
|
|
649
663
|
|
|
650
664
|
self.events.fmt_order = format_order
|
|
651
665
|
|
|
@@ -659,7 +673,7 @@ class Ass:
|
|
|
659
673
|
log.warning("Skip a Style line (illegal format): {}", line)
|
|
660
674
|
continue
|
|
661
675
|
|
|
662
|
-
style_tuple = tuple(
|
|
676
|
+
style_tuple = tuple(map(str.strip, line[6:].split(",")))
|
|
663
677
|
if len(style_tuple) != 23:
|
|
664
678
|
log.warning(
|
|
665
679
|
"Skip a Style line (Style Format len != 23): {}", line
|
|
@@ -709,9 +723,9 @@ class Ass:
|
|
|
709
723
|
continue
|
|
710
724
|
|
|
711
725
|
event_tuple = tuple(
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
",", maxsplit=9
|
|
726
|
+
map(
|
|
727
|
+
str.strip,
|
|
728
|
+
line.split(":", maxsplit=1)[1].split(",", maxsplit=9),
|
|
715
729
|
)
|
|
716
730
|
)
|
|
717
731
|
if len(event_tuple) != 10:
|
|
@@ -726,12 +740,9 @@ class Ass:
|
|
|
726
740
|
|
|
727
741
|
case State.unknown:
|
|
728
742
|
if new_unknown_data is None:
|
|
729
|
-
|
|
730
|
-
"Unknown error occurred when read line: {}",
|
|
731
|
-
line,
|
|
732
|
-
deep=True,
|
|
743
|
+
raise Ass_generate_error(
|
|
744
|
+
"Unknown error occurred when read line: {}", line
|
|
733
745
|
)
|
|
734
|
-
raise Ass_generation_failed()
|
|
735
746
|
new_unknown_data.data.append(line)
|
|
736
747
|
|
|
737
748
|
if new_unknown_data is not None:
|
|
@@ -758,4 +769,4 @@ class Ass:
|
|
|
758
769
|
for data in (() if drop_unkow_data else self.unknown_data)
|
|
759
770
|
),
|
|
760
771
|
)
|
|
761
|
-
return "\n\n".join(
|
|
772
|
+
return "\n\n".join(filter(bool, generator)) + "\n"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import enum
|
|
2
|
+
import itertools
|
|
2
3
|
import os
|
|
3
|
-
import winreg
|
|
4
4
|
from copy import deepcopy
|
|
5
5
|
from dataclasses import dataclass, field
|
|
6
6
|
from pathlib import Path
|
|
@@ -12,6 +12,11 @@ from fontTools.ttLib.tables._n_a_m_e import NameRecord, makeName, table__n_a_m_e
|
|
|
12
12
|
from fontTools.ttLib.ttFont import TTLibError
|
|
13
13
|
|
|
14
14
|
from ...easyrip_log import log
|
|
15
|
+
from ...easyrip_mlang import Mlang_exception
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class Font_error(Mlang_exception):
|
|
19
|
+
pass
|
|
15
20
|
|
|
16
21
|
|
|
17
22
|
class Font_type(enum.Enum):
|
|
@@ -31,33 +36,37 @@ class Font:
|
|
|
31
36
|
def __hash__(self) -> int:
|
|
32
37
|
return hash(self.pathname)
|
|
33
38
|
|
|
34
|
-
def __del__(self):
|
|
39
|
+
def __del__(self) -> None:
|
|
35
40
|
self.font.close()
|
|
36
41
|
|
|
37
42
|
|
|
38
|
-
def load_fonts(
|
|
43
|
+
def load_fonts(
|
|
44
|
+
path: str | Path,
|
|
45
|
+
*,
|
|
46
|
+
lazy: bool = True,
|
|
47
|
+
strict: bool = False,
|
|
48
|
+
) -> list[Font]:
|
|
49
|
+
"""strict: Skip UnicodeDecodeError font file"""
|
|
39
50
|
if isinstance(path, str):
|
|
40
51
|
path = Path(path)
|
|
41
52
|
|
|
42
|
-
res_font_list: Final =
|
|
53
|
+
res_font_list: Final[list[Font]] = []
|
|
43
54
|
|
|
44
55
|
for file in path.iterdir() if path.is_dir() else (path,):
|
|
45
56
|
if not (
|
|
46
57
|
file.is_file()
|
|
47
|
-
and (
|
|
58
|
+
and (suffix := file.suffix.lower()) in {".ttf", ".otf", ".ttc"}
|
|
48
59
|
):
|
|
49
60
|
continue
|
|
50
61
|
|
|
51
62
|
try:
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
]
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
for font in fonts:
|
|
60
|
-
table_name: table__n_a_m_e | None = font.get("name") # type: ignore
|
|
63
|
+
for font in (
|
|
64
|
+
list[TTFont](TTCollection(file=file, lazy=lazy))
|
|
65
|
+
if suffix == ".ttc"
|
|
66
|
+
else [TTFont(file=file, lazy=lazy)]
|
|
67
|
+
):
|
|
68
|
+
skip_this_font: bool = False
|
|
69
|
+
table_name: table__n_a_m_e | None = font.get("name")
|
|
61
70
|
|
|
62
71
|
if table_name is None:
|
|
63
72
|
log.warning(f"No 'name' table found in font {file}")
|
|
@@ -74,14 +83,16 @@ def load_fonts(path: str | Path, lazy: bool = True) -> list[Font]:
|
|
|
74
83
|
if name_id not in {1, 2}:
|
|
75
84
|
continue
|
|
76
85
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
86
|
+
try:
|
|
87
|
+
name_str: str = record.toUnicode()
|
|
88
|
+
except UnicodeDecodeError as e:
|
|
89
|
+
error_text = f"Unicode decode error in font \"{file}\": {e}: '{record.toUnicode('replace')}'. Skip this {'font' if strict else 'name record'}."
|
|
90
|
+
if strict:
|
|
91
|
+
log.error(error_text, is_format=False)
|
|
92
|
+
skip_this_font = True
|
|
93
|
+
break
|
|
94
|
+
log.warning(error_text, is_format=False)
|
|
82
95
|
continue
|
|
83
|
-
else:
|
|
84
|
-
name_str = str(name_str)
|
|
85
96
|
|
|
86
97
|
match name_id:
|
|
87
98
|
case 1: # Font Family Name
|
|
@@ -99,6 +110,9 @@ def load_fonts(path: str | Path, lazy: bool = True) -> list[Font]:
|
|
|
99
110
|
case "italic" | "oblique":
|
|
100
111
|
is_italic = True
|
|
101
112
|
|
|
113
|
+
if skip_this_font:
|
|
114
|
+
continue
|
|
115
|
+
|
|
102
116
|
if not res_font.familys:
|
|
103
117
|
log.warning(f"Font {file} has no family names. Skip this font")
|
|
104
118
|
continue
|
|
@@ -106,7 +120,7 @@ def load_fonts(path: str | Path, lazy: bool = True) -> list[Font]:
|
|
|
106
120
|
if is_regular:
|
|
107
121
|
if is_bold or is_italic:
|
|
108
122
|
log.error(
|
|
109
|
-
|
|
123
|
+
'Font "{}" is Regular but Bold={} and Italic={}. Skip this font',
|
|
110
124
|
file,
|
|
111
125
|
is_bold,
|
|
112
126
|
is_italic,
|
|
@@ -120,70 +134,52 @@ def load_fonts(path: str | Path, lazy: bool = True) -> list[Font]:
|
|
|
120
134
|
else:
|
|
121
135
|
res_font.font_type = Font_type.Regular
|
|
122
136
|
log.warning(
|
|
123
|
-
f
|
|
137
|
+
f'Font "{file}" does not have an English subfamily name. Defaulting to Regular'
|
|
124
138
|
)
|
|
125
139
|
|
|
126
140
|
res_font_list.append(res_font)
|
|
127
141
|
|
|
128
142
|
except TTLibError as e:
|
|
129
|
-
log.
|
|
130
|
-
except UnicodeDecodeError as e:
|
|
131
|
-
log.warning(f"Unicode decode error for font {file}: {e}")
|
|
143
|
+
log.error(f'Failed to load font file "{file}": {e}')
|
|
132
144
|
except Exception as e:
|
|
133
|
-
log.error(f
|
|
145
|
+
log.error(f'Unexpected error when load font "{file}": {e}')
|
|
134
146
|
|
|
135
147
|
return res_font_list
|
|
136
148
|
|
|
137
149
|
|
|
138
|
-
def
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
:
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
with winreg.OpenKey(
|
|
148
|
-
winreg.HKEY_LOCAL_MACHINE,
|
|
149
|
-
r"SOFTWARE\Microsoft\Windows NT\CurrentVersion\Fonts",
|
|
150
|
-
) as key:
|
|
151
|
-
i = 0
|
|
152
|
-
while True:
|
|
153
|
-
try:
|
|
154
|
-
# 枚举所有字体值
|
|
155
|
-
value_name, value_data, _ = winreg.EnumValue(key, i)
|
|
156
|
-
i += 1
|
|
157
|
-
|
|
158
|
-
# 检查字体名称是否匹配(去掉可能的"(TrueType)"等后缀)
|
|
159
|
-
if value_name.startswith(font_name):
|
|
160
|
-
# 获取字体文件路径
|
|
161
|
-
fonts_dir = os.path.join(os.environ["SystemRoot"], "Fonts")
|
|
162
|
-
font_path = os.path.join(fonts_dir, value_data)
|
|
163
|
-
|
|
164
|
-
# 检查文件是否存在
|
|
165
|
-
if os.path.isfile(font_path):
|
|
166
|
-
res.append(font_path)
|
|
167
|
-
except OSError:
|
|
168
|
-
# 没有更多条目时退出循环
|
|
169
|
-
break
|
|
170
|
-
except Exception as e:
|
|
171
|
-
log.warning("Error accessing registry: {}", e)
|
|
150
|
+
def load_windows_fonts(
|
|
151
|
+
*,
|
|
152
|
+
lazy: bool = True,
|
|
153
|
+
strict: bool = False,
|
|
154
|
+
) -> list[Font]:
|
|
155
|
+
paths: tuple[str, ...] = (
|
|
156
|
+
os.path.join(os.environ["SYSTEMROOT"], "Fonts"),
|
|
157
|
+
os.path.join(os.environ["LOCALAPPDATA"], "Microsoft/Windows/Fonts"),
|
|
158
|
+
)
|
|
172
159
|
|
|
173
|
-
return
|
|
160
|
+
return list(
|
|
161
|
+
itertools.chain.from_iterable(
|
|
162
|
+
load_fonts(path, lazy=lazy, strict=strict) for path in paths
|
|
163
|
+
)
|
|
164
|
+
)
|
|
174
165
|
|
|
175
166
|
|
|
176
|
-
def subset_font(font: Font, subset_str: str,
|
|
167
|
+
def subset_font(font: Font, subset_str: str, affix: str) -> tuple[TTFont, bool]:
|
|
177
168
|
subset_font = deepcopy(font.font)
|
|
178
169
|
|
|
179
170
|
# 检查哪些字符不存在于字体中
|
|
180
|
-
|
|
181
|
-
|
|
171
|
+
try:
|
|
172
|
+
cmap = subset_font.getBestCmap()
|
|
173
|
+
if cmap is None:
|
|
174
|
+
raise Exception("cmap is None")
|
|
175
|
+
available_chars = set(map(chr, cmap))
|
|
176
|
+
except Exception as e:
|
|
177
|
+
raise Font_error("Can not read best cmap from '{}'", font.pathname) from e
|
|
182
178
|
input_chars = set(subset_str)
|
|
183
179
|
missing_chars = input_chars - available_chars
|
|
184
180
|
|
|
185
181
|
if missing_chars:
|
|
186
|
-
# 将缺失字符按Unicode码点排序
|
|
182
|
+
# 将缺失字符按 Unicode 码点排序
|
|
187
183
|
sorted_missing = sorted(missing_chars, key=lambda c: ord(c))
|
|
188
184
|
missing_info = ", ".join(f"'{c}' (U+{ord(c):04X})" for c in sorted_missing)
|
|
189
185
|
log.warning(
|
|
@@ -194,17 +190,8 @@ def subset_font(font: Font, subset_str: str, afffix: str) -> tuple[TTFont, bool]
|
|
|
194
190
|
|
|
195
191
|
# 创建子集化选项
|
|
196
192
|
options = subset.Options()
|
|
197
|
-
options.drop_tables = ["DSIG", "PCLT", "EBDT", "EBSC"] # 不移除任何可能有用的表
|
|
198
193
|
options.hinting = True # 保留 hinting
|
|
199
194
|
options.name_IDs = [] # 不保留 name 表记录
|
|
200
|
-
options.no_subset_tables = subset.Options._no_subset_tables_default + [
|
|
201
|
-
"BASE",
|
|
202
|
-
"mort",
|
|
203
|
-
]
|
|
204
|
-
# options.drop_tables = []
|
|
205
|
-
options.name_legacy = True
|
|
206
|
-
# options.retain_gids = True
|
|
207
|
-
options.layout_features = ["*"]
|
|
208
195
|
|
|
209
196
|
# 创建子集化器
|
|
210
197
|
subsetter = subset.Subsetter(options=options)
|
|
@@ -216,10 +203,15 @@ def subset_font(font: Font, subset_str: str, afffix: str) -> tuple[TTFont, bool]
|
|
|
216
203
|
subsetter.subset(subset_font)
|
|
217
204
|
|
|
218
205
|
# 修改 Name Record
|
|
219
|
-
affix_ascii =
|
|
220
|
-
affix_utf16be =
|
|
221
|
-
table_name: table__n_a_m_e = font.font.get("name")
|
|
222
|
-
subset_table_name: table__n_a_m_e = subset_font.get("name")
|
|
206
|
+
affix_ascii = affix.encode("ascii")
|
|
207
|
+
affix_utf16be = affix.encode("utf-16-be")
|
|
208
|
+
table_name: table__n_a_m_e | None = font.font.get("name")
|
|
209
|
+
subset_table_name: table__n_a_m_e | None = subset_font.get("name")
|
|
210
|
+
|
|
211
|
+
# 这两个都是复制的,所以不可能是 None
|
|
212
|
+
assert table_name is not None
|
|
213
|
+
assert subset_table_name is not None
|
|
214
|
+
|
|
223
215
|
subset_table_name.names = list[NameRecord]() # 重写 name table
|
|
224
216
|
for record in table_name.names:
|
|
225
217
|
name_id = int(record.nameID)
|
|
@@ -244,4 +236,5 @@ def subset_font(font: Font, subset_str: str, afffix: str) -> tuple[TTFont, bool]
|
|
|
244
236
|
)
|
|
245
237
|
)
|
|
246
238
|
|
|
239
|
+
subset_font.close()
|
|
247
240
|
return subset_font, not missing_chars
|