Skip to content

Commit 661f009

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 doesn't exist anymore. Now, use MKVUtils.MKVTOOLNIX_FOLDER to set the folder of mkvtoolnix. - Mkvpropedit class have been renamed MKVPropedit.
1 parent a226591 commit 661f009

23 files changed

Lines changed: 957 additions & 337 deletions

README.md

Lines changed: 33 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -12,42 +12,36 @@ This tool allows to recover and/or mux the fonts necessary in an mkv.
1212
pip install FontCollector
1313
```
1414
## Dependencies
15-
- [MKVToolNix](https://www.fosshub.com/MKVToolNix.html)
15+
- [MKVToolNix](https://mkvtoolnix.download/downloads.html)
1616

17-
## Usage
17+
## FontCollector Usage
1818
```console
1919
$ fontcollector --help
20-
usage: fontcollector [-h] --input [INPUT ...] [-mkv MKV] [--output OUTPUT] [-mkvpropedit MKVPROPEDIT] [--delete-fonts] [--additional-fonts ADDITIONAL_FONTS [ADDITIONAL_FONTS ...]]
21-
[--additional-fonts-recursive ADDITIONAL_FONTS_RECURSIVE [ADDITIONAL_FONTS_RECURSIVE ...]] [--exclude-system-fonts] [--collect-draw-fonts] [--dont-convert-variable-to-collection]
22-
[--logging [LOGGING]]
20+
usage: fontcollector [-h] --input INPUT [INPUT ...] [-mkv MKV] [--output OUTPUT] [-mkvtoolnix MKVTOOLNIX] [--delete-fonts] [--additional-fonts ADDITIONAL_FONTS [ADDITIONAL_FONTS ...]]
21+
[--additional-fonts-recursive ADDITIONAL_FONTS_RECURSIVE [ADDITIONAL_FONTS_RECURSIVE ...]] [--exclude-system-fonts] [--collect-draw-fonts] [--dont-convert-variable-to-collection] [--logging [LOGGING]]
2322

2423
FontCollector for Advanced SubStation Alpha file.
2524

2625
options:
2726
-h, --help show this help message and exit
28-
--input [INPUT ...], -i [INPUT ...]
27+
--input INPUT [INPUT ...], -i INPUT [INPUT ...]
2928
Subtitles file. Must be an ASS file/directory. You can specify more than one .ass file/path.
3029
-mkv MKV
3130
Video where the fonts will be merge. Must be a Matroska file.
3231
--output OUTPUT, -o OUTPUT
3332
Destination path of the font. If -o and -mkv aren't specified, it will be the current path.
34-
-mkvpropedit MKVPROPEDIT
35-
Path to mkvpropedit.exe if not in variable environments. If -mkv is not specified, it will do
36-
nothing.
33+
-mkvtoolnix MKVTOOLNIX
34+
Path to the MKVToolNix folder if not in variable environments. If -mkv is not specified, it will do nothing.
3735
--delete-fonts, -d
38-
If -d is specified, it will delete the font attached to the mkv before merging the new needed
39-
font. If -mkv is not specified, it will do nothing.
36+
If -d is specified, it will delete the font attached to the mkv before merging the new needed font. If -mkv is not specified, it will do nothing.
4037
--additional-fonts ADDITIONAL_FONTS [ADDITIONAL_FONTS ...], -add-fonts ADDITIONAL_FONTS [ADDITIONAL_FONTS ...]
41-
May be a directory containing font files or a single font file. You can specify more than one
42-
additional-fonts.
43-
If it is a directory, it won't search recursively for fonts
38+
May be a directory containing font files or a single font file. You can specify more than one additional-fonts. If it is a directory, it won't search recursively for fonts
4439
--additional-fonts-recursive ADDITIONAL_FONTS_RECURSIVE [ADDITIONAL_FONTS_RECURSIVE ...], -add-fonts-rec ADDITIONAL_FONTS_RECURSIVE [ADDITIONAL_FONTS_RECURSIVE ...]
4540
Path to font directory, which will be recursively searched for fonts.
4641
--exclude-system-fonts
4742
If specified, FontCollector won't use the system font to find the font used by an .ass file.
4843
--collect-draw-fonts
49-
If specified, FontCollector will collect the font used by the draw. For more detail when this
50-
is usefull, see: https://github.com/libass/libass/issues/617
44+
If specified, FontCollector will collect the font used by the draw. For more detail when this is usefull, see: https://github.com/libass/libass/issues/617
5145
--dont-convert-variable-to-collection
5246
If specified, FontCollector won't convert variable font to a font collection. see: https://github.com/libass/libass/issues/386
5347
--logging [LOGGING], -log [LOGGING]
@@ -64,12 +58,33 @@ fontCollector -i .
6458
```
6559
Mux font from .ass file into an mkv
6660
```
67-
fontCollector -i "file1.ass" -mkv "example.mkv" -mkvpropedit "C:\Program Files\MKVToolNix\mkvpropedit.exe"
61+
fontCollector -i "file1.ass" -mkv "example.mkv" -mkvtoolnix "C:\Program Files\MKVToolNix"
6862
```
6963

7064
Mux the font from the .ass file into an mkv and delete the currently attached fonts.
7165
```
72-
fontCollector -i "file1.ass" -mkv "example.mkv" -mkvpropedit "C:\Program Files\MKVToolNix\mkvpropedit.exe" -d
66+
fontCollector -i "file1.ass" -mkv "example.mkv" -mkvtoolnix "C:\Program Files\MKVToolNix" -d
67+
```
68+
## MKVFontValidator Usage
69+
```console
70+
$ mkvfontvalidator --help
71+
usage: mkvfontvalidator [-h] -mkv MKV [-mkvtoolnix MKVTOOLNIX] [--need-draw-fonts] [--delete-fonts-not-used] [--logging [LOGGING]]
72+
73+
MKV font validator for Advanced SubStation Alpha file.
74+
75+
options:
76+
-h, --help
77+
show this help message and exit
78+
-mkv MKV
79+
The video file to be verified. Must be a Matroska file.
80+
-mkvtoolnix MKVTOOLNIX
81+
Path to the MKVToolNix folder if not in variable environments.
82+
--need-draw-fonts
83+
If specified, MKVFontValidator 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
84+
--delete-fonts-not-used, -d
85+
If specified, MKVFontValidator will remove the fonts that aren't used by the subtitle(s) of the mkv file.
86+
--logging [LOGGING], -log [LOGGING]
87+
Destination path of log. If it isn't specified, it will be YYYY-MM-DD--HH-MM-SS_mkvfontvalidator.log.
7388
```
7489
## Variable Font
7590
Since [Libass](https://github.com/libass/libass/issues/386) does not support [variable font](https://docs.microsoft.com/en-us/typography/opentype/spec/otvaroverview), this tool will automatically generate a [OpenType Font Collection](https://docs.microsoft.com/en-us/typography/opentype/spec/otff#font-collections). The generated collection is designed to simulate how [VSFilter](https://en.wikipedia.org/wiki/DirectVobSub)/[GDI](https://en.wikipedia.org/wiki/Graphics_Device_Interface) handles variable font.

examples/collect_font_and_mux_them.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
FontFile,
99
FontLoader,
1010
FontSelectionStrategyLibass,
11-
Mkvpropedit,
11+
MKVPropedit,
1212
set_loglevel
1313
)
1414

@@ -47,9 +47,9 @@ 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

52-
Mkvpropedit.merge_fonts_into_mkv(fonts_file_found, mkv_path)
52+
MKVPropedit.merge_fonts_into_mkv(fonts_file_found, mkv_path)
5353

5454

5555
if __name__ == "__main__":

font_collector/__init__.py

Lines changed: 4 additions & 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

@@ -16,6 +16,9 @@ def __init__(self) -> None:
1616
super().__init__("%(levelname)s - %(message)s")
1717

1818
def format(self, record: logging.LogRecord) -> str:
19+
if not record.msg:
20+
return ""
21+
1922
s = super().format(record)
2023
head, *tail = s.splitlines()
2124
indent = " " * (len(record.levelname) + 3) # "LEVEL - " length

font_collector/__main__.py

Lines changed: 7 additions & 56 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,62 +49,14 @@ 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 {'' if style.italic else 'non-'}italic, got {'' if font_result.font_face.is_italic else 'non-'}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))
54+
_logger.info("")
10455

10556
if mkv_path is not None:
10657
if delete_fonts:
107-
Mkvpropedit.delete_fonts_of_mkv(mkv_path)
108-
Mkvpropedit.merge_fonts_into_mkv(fonts_file_found, mkv_path)
58+
MKVPropedit.delete_all_fonts_of_mkv(mkv_path)
59+
MKVPropedit.merge_fonts_into_mkv(fonts_file_found, mkv_path)
10960
else:
11061
if not output_directory.is_dir():
11162
output_directory.mkdir()

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 {'' if style.italic else 'non-'}italic, got {'' if font_result.font_face.is_italic else 'non-'}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+

0 commit comments

Comments
 (0)