Files
DroidDeck/tools/tests/test_droiddeck_fonts.py
0x142857andxXJsonDeruloXx 7c08cb58ae Share font defaults across runtime sessions (#252)
* Support CJK text with the device's own fonts

The Linux runtime's rootfs ships DejaVu alone, which has no CJK
glyphs, so Chinese, Japanese and Korean text in Steam Big Picture and
on the LXQt desktop draws as tofu boxes (issues #118, #206, #249).

Bind the device's own CJK fonts - Android's world-readable Noto CJK
.ttc, or DroidSansFallback / Source Han - into the guest under
/usr/share/fonts/droiddeck-cjk, so nothing is added to the ~790 MB
runtime image and the coverage follows the device. A new
droiddeck-fonts script, run by droiddeck-session before any program
that queries fontconfig, links the default rules, writes local.conf
with DejaVu first and the CJK families after it, and rebuilds the
fontconfig cache once per set of font files. The desktop's own
fontconfig block moves there, so every mode gets it.

* Fix CJK and Thai: bind the device font directory, not one file at a time

The first cut bound each device font with its own proot -b host:guest. A
per-file binding into a plain directory is not applied here: the guest sees a
zero-byte placeholder where the font should be (stat size=0, fc-scan empty),
so Chinese stayed tofu even though the app had "bound" the font. A directory
binding is what the rest of the runtime relies on, so bind /system/fonts as a
whole under /usr/share/fonts/droiddeck-device. That also brings Thai, Arabic,
Devanagari and the emoji fonts, which the runtime did not have either.

Verified on the AYN Thor: the session's fontconfig probe reports
sans=DejaVu Sans, zh=Noto Sans CJK SC, th=Noto Serif Thai, and Steam Big
Picture in Simplified Chinese renders without tofu (screenshot under
docs/development/evidence/).

---------

Co-authored-by: xXJsonDeruloXx <[email protected]>
2026-10-08 04:06:23 +00:00

77 lines
3.3 KiB
Python

import subprocess
import tempfile
import unittest
from pathlib import Path
FONTS = Path(__file__).resolve().parents[1] / "linuxfs/overlay/usr/local/bin/droiddeck-fonts"
SESSION = Path(__file__).resolve().parents[1] / "linuxfs/overlay/usr/local/bin/droiddeck-session"
class DroidDeckFontsTest(unittest.TestCase):
def setUp(self):
temporary = tempfile.TemporaryDirectory()
self.addCleanup(temporary.cleanup)
self.root = Path(temporary.name)
defaults = self.root / "usr/share/fontconfig/conf.default"
defaults.mkdir(parents=True)
(defaults / "60-latin.conf").write_text("<fontconfig/>")
dejavu = self.root / "usr/share/fonts/truetype/dejavu"
dejavu.mkdir(parents=True)
(dejavu / "DejaVuSans.ttf").write_text("dejavu")
def run_fonts(self):
subprocess.run(["bash", str(FONTS), str(self.root)], check=True, capture_output=True)
def local_conf(self):
return (self.root / "etc/fonts/local.conf").read_text()
def owned_conf(self):
return (self.root / "etc/fonts/conf.d/65-droiddeck-default-fonts.conf").read_text()
def test_default_rules_linked_and_dejavu_is_the_default(self):
self.run_fonts()
self.assertTrue((self.root / "etc/fonts/conf.d/60-latin.conf").is_symlink())
text = self.owned_conf()
self.assertIn("<family>sans-serif</family><prefer><family>DejaVu Sans</family>", text)
self.assertIn("<family>monospace</family><prefer><family>DejaVu Sans Mono</family>", text)
self.assertNotIn("Noto Sans CJK", text)
self.assertFalse((self.root / "etc/fonts/local.conf").exists())
def test_an_existing_conf_d_rule_is_not_replaced(self):
conf_d = self.root / "etc/fonts/conf.d"
conf_d.mkdir(parents=True)
(conf_d / "60-latin.conf").write_text("mine")
self.run_fonts()
self.assertFalse((conf_d / "60-latin.conf").is_symlink())
self.assertEqual("mine", (conf_d / "60-latin.conf").read_text())
def test_custom_local_conf_survives_and_device_language_fallback_remains_configured(self):
custom = self.root / "etc/fonts/local.conf"
custom.parent.mkdir(parents=True, exist_ok=True)
custom.write_text("<fontconfig><rescan><int>45</int></rescan></fontconfig>")
self.run_fonts()
self.assertEqual("<fontconfig><rescan><int>45</int></rescan></fontconfig>", self.local_conf())
session = SESSION.read_text()
self.assertIn('device_fonts=/usr/local/share/fonts/android', session)
self.assertIn('zh-cn=Noto Sans CJK SC', session)
self.assertIn('ja=Noto Sans CJK JP', session)
self.assertIn('ko=Noto Sans CJK KR', session)
self.assertIn('th=Noto Sans Thai', session)
def test_unchanged_fonts_do_not_rewrite_owned_conf(self):
self.run_fonts()
owned = self.root / "etc/fonts/conf.d/65-droiddeck-default-fonts.conf"
before = owned.stat().st_mtime_ns
self.run_fonts()
self.assertEqual(before, owned.stat().st_mtime_ns)
def test_the_session_runs_the_bootstrap_before_it_starts_a_program(self):
text = SESSION.read_text()
self.assertIn("/usr/local/bin/droiddeck-fonts", text)
self.assertLess(text.index("droiddeck-fonts"), text.index("droiddeck-steam-install"))
if __name__ == "__main__":
unittest.main()