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