xref: /linux/tools/lib/python/kdoc/latex_fonts.py (revision 37a93dd5c49b5fda807fd204edf2547c3493319c)
1#!/usr/bin/env python3
2# SPDX-License-Identifier: GPL-2.0-only
3# Copyright (C) Akira Yokosawa, 2024
4#
5# Ported to Python by (c) Mauro Carvalho Chehab, 2025
6
7"""
8Detect problematic Noto CJK variable fonts
9==========================================
10
11For ``make pdfdocs``, reports of build errors of translations.pdf started
12arriving early 2024 [1]_ [2]_.  It turned out that Fedora and openSUSE
13tumbleweed have started deploying variable-font [3]_ format of "Noto CJK"
14fonts [4]_ [5]_.  For PDF, a LaTeX package named xeCJK is used for CJK
15(Chinese, Japanese, Korean) pages.  xeCJK requires XeLaTeX/XeTeX, which
16does not (and likely never will) understand variable fonts for historical
17reasons.
18
19The build error happens even when both of variable- and non-variable-format
20fonts are found on the build system.  To make matters worse, Fedora enlists
21variable "Noto CJK" fonts in the requirements of langpacks-ja, -ko, -zh_CN,
22-zh_TW, etc.  Hence developers who have interest in CJK pages are more
23likely to encounter the build errors.
24
25This script is invoked from the error path of "make pdfdocs" and emits
26suggestions if variable-font files of "Noto CJK" fonts are in the list of
27fonts accessible from XeTeX.
28
29.. [1] https://lore.kernel.org/r/8734tqsrt7.fsf@meer.lwn.net/
30.. [2] https://lore.kernel.org/r/1708585803.600323099@f111.i.mail.ru/
31.. [3] https://en.wikipedia.org/wiki/Variable_font
32.. [4] https://fedoraproject.org/wiki/Changes/Noto_CJK_Variable_Fonts
33.. [5] https://build.opensuse.org/request/show/1157217
34
35Workarounds for building translations.pdf
36-----------------------------------------
37
38* Denylist "variable font" Noto CJK fonts.
39
40  - Create $HOME/deny-vf/fontconfig/fonts.conf from template below, with
41    tweaks if necessary.  Remove leading "".
42
43  - Path of fontconfig/fonts.conf can be overridden by setting an env
44    variable FONTS_CONF_DENY_VF.
45
46    * Template::
47
48        <?xml version="1.0"?>
49        <!DOCTYPE fontconfig SYSTEM "urn:fontconfig:fonts.dtd">
50        <fontconfig>
51        <!--
52        Ignore variable-font glob (not to break xetex)
53        -->
54            <selectfont>
55                <rejectfont>
56                    <!--
57                        for Fedora
58                    -->
59                    <glob>/usr/share/fonts/google-noto-*-cjk-vf-fonts</glob>
60                    <!--
61                        for openSUSE tumbleweed
62                    -->
63                    <glob>/usr/share/fonts/truetype/Noto*CJK*-VF.otf</glob>
64                </rejectfont>
65            </selectfont>
66        </fontconfig>
67
68    The denylisting is activated for "make pdfdocs".
69
70* For skipping CJK pages in PDF
71
72  - Uninstall texlive-xecjk.
73    Denylisting is not needed in this case.
74
75* For printing CJK pages in PDF
76
77  - Need non-variable "Noto CJK" fonts.
78
79    * Fedora
80
81      - google-noto-sans-cjk-fonts
82      - google-noto-serif-cjk-fonts
83
84    * openSUSE tumbleweed
85
86      - Non-variable "Noto CJK" fonts are not available as distro packages
87        as of April, 2024.  Fetch a set of font files from upstream Noto
88        CJK Font released at:
89
90          https://github.com/notofonts/noto-cjk/tree/main/Sans#super-otc
91
92        and at:
93
94          https://github.com/notofonts/noto-cjk/tree/main/Serif#super-otc
95
96        then uncompress and deploy them.
97      - Remember to update fontconfig cache by running fc-cache.
98
99.. caution::
100    Uninstalling "variable font" packages can be dangerous.
101    They might be depended upon by other packages important for your work.
102    Denylisting should be less invasive, as it is effective only while
103    XeLaTeX runs in "make pdfdocs".
104"""
105
106import os
107import re
108import subprocess
109import textwrap
110import sys
111
112class LatexFontChecker:
113    """
114    Detect problems with CJK variable fonts that affect PDF builds for
115    translations.
116    """
117
118    def __init__(self, deny_vf=None):
119        if not deny_vf:
120            deny_vf = os.environ.get('FONTS_CONF_DENY_VF', "~/deny-vf")
121
122        self.environ = os.environ.copy()
123        self.environ['XDG_CONFIG_HOME'] = os.path.expanduser(deny_vf)
124
125        self.re_cjk = re.compile(r"([^:]+):\s*Noto\s+(Sans|Sans Mono|Serif) CJK")
126
127    def description(self):
128        """
129        Returns module description.
130        """
131        return __doc__
132
133    def get_noto_cjk_vf_fonts(self):
134        """
135        Get Noto CJK fonts.
136        """
137
138        cjk_fonts = set()
139        cmd = ["fc-list", ":", "file", "family", "variable"]
140        try:
141            result = subprocess.run(cmd,stdout=subprocess.PIPE,
142                                    stderr=subprocess.PIPE,
143                                    universal_newlines=True,
144                                    env=self.environ,
145                                    check=True)
146
147        except subprocess.CalledProcessError as exc:
148            sys.exit(f"Error running fc-list: {repr(exc)}")
149
150        for line in result.stdout.splitlines():
151            if 'variable=True' not in line:
152                continue
153
154            match = self.re_cjk.search(line)
155            if match:
156                cjk_fonts.add(match.group(1))
157
158        return sorted(cjk_fonts)
159
160    def check(self):
161        """
162        Check for problems with CJK fonts.
163        """
164
165        fonts = textwrap.indent("\n".join(self.get_noto_cjk_vf_fonts()), "    ")
166        if not fonts:
167            return None
168
169        rel_file = os.path.relpath(__file__, os.getcwd())
170
171        msg = "=" * 77 + "\n"
172        msg += 'XeTeX is confused by "variable font" files listed below:\n'
173        msg += fonts + "\n"
174        msg += textwrap.dedent(f"""
175                For CJK pages in PDF, they need to be hidden from XeTeX by denylisting.
176                Or, CJK pages can be skipped by uninstalling texlive-xecjk.
177
178                For more info on denylisting, other options, and variable font, run:
179
180                    tools/docs/check-variable-fonts.py -h
181            """)
182        msg += "=" * 77
183
184        return msg
185