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.
Files changed (36) hide show
  1. easyrip/__init__.py +5 -1
  2. easyrip/__main__.py +124 -15
  3. easyrip/easyrip_command.py +457 -148
  4. easyrip/easyrip_config/config.py +269 -0
  5. easyrip/easyrip_config/config_key.py +28 -0
  6. easyrip/easyrip_log.py +120 -42
  7. easyrip/easyrip_main.py +509 -259
  8. easyrip/easyrip_mlang/__init__.py +20 -45
  9. easyrip/easyrip_mlang/global_lang_val.py +18 -16
  10. easyrip/easyrip_mlang/lang_en.py +1 -1
  11. easyrip/easyrip_mlang/lang_zh_Hans_CN.py +101 -77
  12. easyrip/easyrip_mlang/translator.py +12 -10
  13. easyrip/easyrip_prompt.py +73 -0
  14. easyrip/easyrip_web/__init__.py +2 -1
  15. easyrip/easyrip_web/http_server.py +56 -42
  16. easyrip/easyrip_web/third_party_api.py +60 -8
  17. easyrip/global_val.py +21 -1
  18. easyrip/ripper/media_info.py +10 -3
  19. easyrip/ripper/param.py +482 -0
  20. easyrip/ripper/ripper.py +260 -574
  21. easyrip/ripper/sub_and_font/__init__.py +10 -0
  22. easyrip/ripper/{font_subset → sub_and_font}/ass.py +95 -84
  23. easyrip/ripper/{font_subset → sub_and_font}/font.py +72 -79
  24. easyrip/ripper/{font_subset → sub_and_font}/subset.py +122 -81
  25. easyrip/utils.py +129 -27
  26. easyrip-4.9.1.dist-info/METADATA +92 -0
  27. easyrip-4.9.1.dist-info/RECORD +31 -0
  28. easyrip/easyrip_config.py +0 -198
  29. easyrip/ripper/__init__.py +0 -10
  30. easyrip/ripper/font_subset/__init__.py +0 -7
  31. easyrip-3.13.2.dist-info/METADATA +0 -89
  32. easyrip-3.13.2.dist-info/RECORD +0 -29
  33. {easyrip-3.13.2.dist-info → easyrip-4.9.1.dist-info}/WHEEL +0 -0
  34. {easyrip-3.13.2.dist-info → easyrip-4.9.1.dist-info}/entry_points.txt +0 -0
  35. {easyrip-3.13.2.dist-info → easyrip-4.9.1.dist-info}/licenses/LICENSE +0 -0
  36. {easyrip-3.13.2.dist-info → easyrip-4.9.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,10 @@
1
+ from .ass import Ass
2
+ from .font import Font
3
+ from .subset import load_fonts, subset
4
+
5
+ __all__ = [
6
+ "Ass",
7
+ "Font",
8
+ "load_fonts",
9
+ "subset",
10
+ ]
@@ -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 Ass_generation_failed(
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 Ass_generation_failed(e)
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
- 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
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
- if not use_libass_spec:
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
- result = []
336
- current = list[str]() # 当前累积的字符
337
- in_tag = False # 是否在标签内
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 escape_next:
315
+ if is_escape_next:
342
316
  # 处理转义字符(任何字符直接作为普通字符)
343
317
  current.append(char)
344
- escape_next = False
318
+ is_escape_next = False
345
319
  elif char == "\\":
346
320
  # 标记下一个字符为转义
347
321
  current.append(char)
348
- escape_next = True
322
+ is_escape_next = True
349
323
  elif char == "{":
350
- if not in_tag:
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
- in_tag = True
330
+ is_in_tag = True
357
331
  else:
358
332
  current.append(char) # 标签内的{
359
333
  elif char == "}":
360
- if in_tag:
334
+ if is_in_tag:
361
335
  # 非转义的}结束标签
362
336
  current.append(char)
363
337
  result.append((True, "".join(current)))
364
338
  current = []
365
- in_tag = False
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 Ass_generation_failed(
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 Ass_generation_failed(e)
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 Ass_generation_failed(Exception):
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
- log.error("Not a file: {}", path)
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
- formats_generator = (v.strip() for v in line[7:].split(","))
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(v) for v in formats_generator)
641
+ format_order = tuple(map(Style_fmt_it, formats_tuple))
640
642
  if len(format_order) != 23:
641
- raise Ass_generation_failed("Style Format len != 23")
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
- format_order = tuple(Event_fmt_it(v) for v in formats_generator)
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 Ass_generation_failed("Event Format != 10")
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(v.strip() for v in line[6:].split(","))
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
- v.strip()
713
- for v in line.split(":", maxsplit=1)[1].split(
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
- log.error(
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(v for v in generator if v) + "\n"
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(path: str | Path, lazy: bool = True) -> list[Font]:
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 = list[Font]()
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 ((suffix := file.suffix.lower()) in {".ttf", ".otf", ".ttc"})
58
+ and (suffix := file.suffix.lower()) in {".ttf", ".otf", ".ttc"}
48
59
  ):
49
60
  continue
50
61
 
51
62
  try:
52
- if suffix == ".ttc":
53
- fonts: list[TTFont] = [
54
- font for font in TTCollection(file=file, lazy=lazy)
55
- ]
56
- else:
57
- fonts = [TTFont(file=file, lazy=lazy)]
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
- name_str: str = record.toUnicode()
78
- if not isinstance(name_str, str):
79
- log.warning(
80
- f"Unexpected type for name string in font {file}: {type(name_str)}"
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
- "Font {} is Regular but Bold={} and Italic={}. Skip this font",
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"Font {file} does not have an English subfamily name. Defaulting to Regular"
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.warning(f'Error loading font file "{file}": {e}')
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"Unexpected error for font {file}: {e}")
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 get_font_path_from_registry(font_name: str) -> list[str]:
139
- """
140
- 通过Windows注册表获取字体文件路径
141
- :param font_name: 字体名称(如"Arial")
142
- :return: 字体文件完整路径,如果找不到返回None
143
- """
144
- res: Final = list[str]()
145
- try:
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 res
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, afffix: str) -> tuple[TTFont, bool]:
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
- cmap = subset_font.getBestCmap()
181
- available_chars = set(chr(key) for key in cmap.keys())
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 = afffix.encode("ascii")
220
- affix_utf16be = afffix.encode("utf-16-be")
221
- table_name: table__n_a_m_e = font.font.get("name") # type: ignore
222
- subset_table_name: table__n_a_m_e = subset_font.get("name") # type: ignore
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