Skip to content

Commit d8657f6

Browse files
committed
Add mkvfontvalidator
Close #49 Breaking changes: - Mkvpropedit.delete_fonts_of_mkv have been renamed Mkvpropedit.delete_all_fonts_of_mkv - Mkvpropedit.path have been renamed Mkvpropedit.PROGRAM_PATH
1 parent 3d7c718 commit d8657f6

21 files changed

Lines changed: 779 additions & 239 deletions

examples/collect_font_and_mux_them.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ def main():
4747

4848

4949
# If the mkv already contains font, you can remove them
50-
Mkvpropedit.delete_fonts_of_mkv(mkv_path)
50+
Mkvpropedit.delete_all_fonts_of_mkv(mkv_path)
5151

5252
Mkvpropedit.merge_fonts_into_mkv(fonts_file_found, mkv_path)
5353

font_collector/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@
33
# Packages
44
from .ass import *
55
from .font import *
6+
from .mkvtoolnix import *
67
from .system_lang import *
78
# Files
89
from .exceptions import *
9-
from .mkvpropedit import *
1010
from ._version import __version__
1111
from fontTools.misc.loggingTools import configLogger
1212

font_collector/__main__.py

Lines changed: 5 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,14 @@
55

66
from . import _handler
77
from .ass.ass_document import AssDocument
8+
from .collect_fonts import collect_subtitle_fonts
89
from .font import (
910
FontCollection,
1011
FontFile,
1112
FontLoader,
12-
FontSelectionStrategyLibass,
13-
VariableFontFace,
14-
font_weight_to_name
13+
FontSelectionStrategyLibass
1514
)
16-
from .mkvpropedit import Mkvpropedit
15+
from .mkvtoolnix.mkvpropedit import Mkvpropedit
1716
from .parse_arguments import parse_arguments
1817

1918
_logger = logging.getLogger(__name__)
@@ -50,61 +49,12 @@ def main() -> None:
5049
for ass_path in ass_files_path:
5150
subtitle = AssDocument.from_file(ass_path)
5251
_logger.info(f"Loaded successfully {ass_path}")
53-
used_styles = subtitle.get_used_style(collect_draw_fonts)
5452

55-
nbr_font_not_found = 0
56-
57-
for style, usage_data in used_styles.items():
58-
59-
font_result = font_collection.get_used_font_by_style(style, font_strategy)
60-
61-
# Did not found the font
62-
if font_result is None:
63-
nbr_font_not_found += 1
64-
_logger.error(
65-
f"Could not find font '{style.fontname}'\n"
66-
f"Used on lines: {' '.join(str(line) for line in usage_data.ordered_lines)}"
67-
)
68-
else:
69-
log_msg = ""
70-
if font_result.need_faux_bold:
71-
log_msg = f"Faux bold used for '{style.fontname}' (requested weight {style.weight}-{(font_weight_to_name(style.weight))}, got {font_result.font_face.weight}-{(font_weight_to_name(font_result.font_face.weight))})."
72-
elif font_result.mismatch_bold:
73-
log_msg = f"Mismatched weight for '{style.fontname}' (requested weight {style.weight}-{(font_weight_to_name(style.weight))}, got {font_result.font_face.weight}-{(font_weight_to_name(font_result.font_face.weight))})."
74-
if font_result.mismatch_italic:
75-
log_msg = f"Mismatched italic for '{style.fontname}' (requested {'non-' if style.italic else ''}italic, got {'non-' if font_result.font_face.is_italic else ''}italic)."
76-
77-
if log_msg:
78-
_logger.warning(
79-
f"{log_msg}\n"
80-
f"Used on lines: {' '.join(str(line) for line in usage_data.ordered_lines)}"
81-
)
82-
83-
84-
missing_glyphs = font_result.font_face.get_missing_glyphs(usage_data.characters_used)
85-
if len(missing_glyphs) > 0:
86-
_logger.warning(f"'{style.fontname}' is missing the following glyphs used: {missing_glyphs}")
87-
88-
89-
if font_result.font_face.font_file is None:
90-
raise ValueError(f"This font_face \"{font_result.font_face}\" isn't linked to any FontFile.")
91-
92-
if convert_variable_to_collection and isinstance(font_result.font_face, VariableFontFace):
93-
font_name = font_result.font_face.get_best_family_prefix_from_lang().value
94-
font_filename = output_directory.joinpath(f"{font_name}.ttc")
95-
generated_font_file = font_result.font_face.variable_font_to_collection(font_filename)
96-
fonts_file_found.add(generated_font_file)
97-
else:
98-
fonts_file_found.add(font_result.font_face.font_file)
99-
100-
if nbr_font_not_found == 0:
101-
_logger.info(f"All font(s) found")
102-
else:
103-
_logger.error(f"{nbr_font_not_found} font(s) could not be found.")
53+
fonts_file_found.update(collect_subtitle_fonts(subtitle, font_collection, font_strategy, collect_draw_fonts, convert_variable_to_collection, output_directory))
10454

10555
if mkv_path is not None:
10656
if delete_fonts:
107-
Mkvpropedit.delete_fonts_of_mkv(mkv_path)
57+
Mkvpropedit.delete_all_fonts_of_mkv(mkv_path)
10858
Mkvpropedit.merge_fonts_into_mkv(fonts_file_found, mkv_path)
10959
else:
11060
if not output_directory.is_dir():

font_collector/collect_fonts.py

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import logging
2+
from pathlib import Path
3+
from typing import Optional
4+
5+
from .ass.ass_document import AssDocument
6+
from .font import (
7+
FontCollection,
8+
FontFile,
9+
FontSelectionStrategy,
10+
VariableFontFace,
11+
font_weight_to_name
12+
)
13+
14+
_logger = logging.getLogger(__name__)
15+
16+
17+
def collect_subtitle_fonts(
18+
subtitle: AssDocument,
19+
font_collection: FontCollection,
20+
font_strategy: FontSelectionStrategy,
21+
collect_draw_fonts: bool,
22+
convert_variable_to_collection: bool = False,
23+
output_variable_font_directory: Optional[Path] = None
24+
) -> set[FontFile]:
25+
"""
26+
Collect the fonts used in a given subtitle (ASS) document.
27+
28+
Args:
29+
subtitle (AssDocument): The ASS subtitle document to be analyzed.
30+
font_collection (FontCollection): The collection of available fonts that will be used to match against the subtitle's styles.
31+
font_strategy (FontSelectionStrategy): The strategy used to select the best matching font for each style.
32+
collect_draw_fonts (bool): Whether to include fonts used in ASS drawing commands (`\\pN` commands).
33+
convert_variable_to_collection (bool): If True, variable fonts found in the subtitle will be converted into
34+
a TrueType Collection (TTC) file and written to the specified output directory.
35+
output_variable_font_directory (Optional[Path]): The directory where converted variable fonts will be saved, if applicable.
36+
Required when `convert_variable_to_collection` is True.
37+
38+
Returns:
39+
A set of `FontFile` objects representing all fonts used by the ASS document.
40+
"""
41+
fonts_file_found: set[FontFile] = set()
42+
43+
used_styles = subtitle.get_used_style(collect_draw_fonts)
44+
45+
nbr_font_not_found = 0
46+
47+
for style, usage_data in used_styles.items():
48+
49+
font_result = font_collection.get_used_font_by_style(style, font_strategy)
50+
51+
# Did not found the font
52+
if font_result is None:
53+
nbr_font_not_found += 1
54+
_logger.error(
55+
f"Could not find font '{style.fontname}'\n"
56+
f"Used on lines: {' '.join(str(line) for line in usage_data.ordered_lines)}"
57+
)
58+
else:
59+
log_msg = ""
60+
if font_result.need_faux_bold:
61+
log_msg = f"Faux bold used for '{style.fontname}' (requested weight {style.weight}-{(font_weight_to_name(style.weight))}, got {font_result.font_face.weight}-{(font_weight_to_name(font_result.font_face.weight))})."
62+
elif font_result.mismatch_bold:
63+
log_msg = f"Mismatched weight for '{style.fontname}' (requested weight {style.weight}-{(font_weight_to_name(style.weight))}, got {font_result.font_face.weight}-{(font_weight_to_name(font_result.font_face.weight))})."
64+
if font_result.mismatch_italic:
65+
log_msg = f"Mismatched italic for '{style.fontname}' (requested {'non-' if style.italic else ''}italic, got {'non-' if font_result.font_face.is_italic else ''}italic)."
66+
67+
if log_msg:
68+
_logger.warning(
69+
f"{log_msg}\n"
70+
f"Used on lines: {' '.join(str(line) for line in usage_data.ordered_lines)}"
71+
)
72+
73+
74+
missing_glyphs = font_result.font_face.get_missing_glyphs(usage_data.characters_used)
75+
if len(missing_glyphs) > 0:
76+
_logger.warning(f"'{style.fontname}' is missing the following glyphs used: {missing_glyphs}")
77+
78+
79+
if font_result.font_face.font_file is None:
80+
raise ValueError(f"This font_face \"{font_result.font_face}\" isn't linked to any FontFile.")
81+
82+
if convert_variable_to_collection and isinstance(font_result.font_face, VariableFontFace):
83+
if output_variable_font_directory is None:
84+
raise ValueError("When ``convert_variable_to_collection`` is True, you must provide a value for ``output_variable_font_directory``.")
85+
font_name = font_result.font_face.get_best_family_prefix_from_lang().value
86+
font_filename = output_variable_font_directory.joinpath(f"{font_name}.ttc")
87+
generated_font_file = font_result.font_face.variable_font_to_collection(font_filename)
88+
fonts_file_found.add(generated_font_file)
89+
else:
90+
fonts_file_found.add(font_result.font_face.font_file)
91+
92+
if nbr_font_not_found == 0:
93+
_logger.info(f"All font(s) found")
94+
else:
95+
_logger.error(f"{nbr_font_not_found} font(s) could not be found.")
96+
97+
return fonts_file_found
98+

font_collector/mkvfontvalidator.py

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
import logging
2+
from argparse import ArgumentParser
3+
from datetime import datetime
4+
from pathlib import Path
5+
from sys import argv
6+
from tempfile import TemporaryDirectory
7+
from typing import Optional
8+
9+
from . import _handler
10+
from .ass.ass_document import AssDocument
11+
from .collect_fonts import collect_subtitle_fonts
12+
from .font import (
13+
FontCollection,
14+
FontFile,
15+
FontLoader,
16+
FontSelectionStrategyLibass
17+
)
18+
from .mkvtoolnix import MKVExtract, MKVFontFile, Mkvpropedit
19+
20+
_logger = logging.getLogger(__name__)
21+
22+
23+
def main() -> None:
24+
start_time = datetime.now().strftime("%Y-%m-%d--%H-%M-%S")
25+
default_log_path = Path.cwd().joinpath(f"{start_time}_mkvfontvalidator.log")
26+
27+
parser = ArgumentParser(
28+
description="MKV font validator for Advanced SubStation Alpha file."
29+
)
30+
parser.add_argument(
31+
"-mkv",
32+
type=Path,
33+
help="""
34+
The video file to be verified. Must be a Matroska file.
35+
""",
36+
)
37+
parser.add_argument(
38+
"-mkvpropedit",
39+
type=Path,
40+
help="""
41+
Path to mkvpropedit.exe if not in variable environments.
42+
""",
43+
)
44+
parser.add_argument(
45+
"--need-draw-fonts",
46+
action="store_true",
47+
help="""
48+
If specified, FontCollector will report a error if a font used in a draw isn't muxed to the mkv. For more detail when this is usefull, see: https://github.com/libass/libass/issues/617
49+
""",
50+
)
51+
parser.add_argument(
52+
"--delete-fonts-not-used",
53+
"-d",
54+
action="store_true",
55+
help="""
56+
If specified, FontCollector will remove the fonts that aren't used by the subtitle(s) of the mkv file.
57+
""",
58+
)
59+
parser.add_argument(
60+
"--logging",
61+
"-log",
62+
type=Path,
63+
nargs="?",
64+
const=default_log_path,
65+
default=None,
66+
help="""
67+
Destination path of log. If it isn't specified, it will be YYYY-MM-DD--HH-MM-SS_mkvfontvalidator.log.
68+
""",
69+
)
70+
71+
args = parser.parse_args()
72+
73+
mkv_path: Path = args.mkv
74+
need_draw_fonts: bool = args.need_draw_fonts
75+
delete_fonts_not_used: bool = args.delete_fonts_not_used
76+
logging_file_path: Optional[Path] = args.logging
77+
78+
if args.mkvpropedit:
79+
Mkvpropedit.PROGRAM_PATH = args.mkvpropedit
80+
81+
if logging_file_path:
82+
file_handler = logging.FileHandler(logging_file_path, mode="a", encoding="utf-8")
83+
file_handler.setLevel(_handler.level)
84+
file_handler.setFormatter(_handler.formatter)
85+
root_logger = logging.getLogger()
86+
root_logger.addHandler(file_handler)
87+
_logger.info(f"{Path.cwd()}>{' '.join(argv)}")
88+
89+
try:
90+
with TemporaryDirectory() as tmp_dir:
91+
92+
mkv_ass_files = MKVExtract.get_mkv_ass_files(mkv_path, Path(tmp_dir))
93+
mkv_font_files = MKVExtract.get_mkv_font_files(mkv_path, Path(tmp_dir))
94+
95+
fonts_file_found: set[FontFile] = set()
96+
additional_fonts = FontLoader.load_additional_fonts([f.filename for f in mkv_font_files])
97+
font_collection = FontCollection(use_system_font=False, additional_fonts=additional_fonts)
98+
font_strategy = FontSelectionStrategyLibass()
99+
100+
for mkv_ass_file in mkv_ass_files:
101+
subtitle = AssDocument.from_file(mkv_ass_file.filename)
102+
_logger.info(f"Loaded successfully the .ass stream at index {mkv_ass_file.mkv_id}")
103+
104+
fonts_file_found.update(collect_subtitle_fonts(subtitle, font_collection, font_strategy, need_draw_fonts, False, None))
105+
106+
font_not_used: list[MKVFontFile] = []
107+
for mkv_font_file in mkv_font_files:
108+
if not any(mkv_font_file.filename.samefile(font_file.filename) for font_file in fonts_file_found):
109+
font_not_used.append(mkv_font_file)
110+
111+
if not delete_fonts_not_used:
112+
_logger.warning(f"You can remove the font at the id {mkv_font_file.mkv_id} named \"{mkv_font_file.mkv_font_filename}\". It is not used by any .ass file.")
113+
114+
if delete_fonts_not_used and font_not_used:
115+
Mkvpropedit.delete_fonts_of_mkv(mkv_path, [mkv_font.mkv_id for mkv_font in font_not_used])
116+
_logger.warning(f'Successfully deleted {", ".join([f"{mkv_font.mkv_id}-{mkv_font.mkv_font_filename}" for mkv_font in font_not_used])} of the mkv "{mkv_path}"')
117+
118+
except Exception as e:
119+
_logger.error("An unexpected error occured", exc_info=True)
120+
121+
122+
if __name__ == "__main__":
123+
main()

0 commit comments

Comments
 (0)