From c9391fb894483bec7e87e37d880a744f10323cbf Mon Sep 17 00:00:00 2001 From: Bia503 Date: Thu, 18 Jun 2026 20:47:24 +0800 Subject: [PATCH] fix(arm64-linux): fix CJK font rendering on flutter-elinux (#15324) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(arm64-linux): fix CJK font rendering on flutter-elinux The flutter-elinux engine used for ARM64 Linux builds is compiled without --enable-fontconfig, so Flutter's text shaper cannot discover system fonts. This causes CJK characters to render as tofu boxes even when fonts such as Noto Sans CJK are installed. See flutter/flutter#139293. Fix by loading a CJK font at startup via FontLoader (bypassing fontconfig) and propagating it through two paths so all text widgets are covered: 1. MyTheme.applyFontFallback() — updates textTheme on both light and dark ThemeData so Material components receive the fallback through the theme. 2. _mergeCjkFallback() in GetMaterialApp builders — wraps child widgets in DefaultTextStyle.merge so bare Text() widgets and those with inherit:true also render CJK characters correctly. Font discovery queries fc-list for zh, ja, and ko separately, preferring fonts present in all three sets (true pan-CJK fonts such as NotoSansCJK or SourceHanSans) over Chinese-only fonts that may lack Japanese kana or Korean hangul glyphs. Falls back to a hardcoded search-path list covering Debian/Ubuntu, Fedora/RHEL, Arch Linux, and WenQuanYi font layouts. This is an app-level workaround. The engine-level fix is tracked at flutter/flutter#180235 (open as of 2026-06). Fixes #10666 Signed-off-by: Bia503 * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --------- Signed-off-by: Bia503 Co-authored-by: RustDesk <71636191+rustdesk@users.noreply.github.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- flutter/lib/common.dart | 16 ++++ flutter/lib/main.dart | 22 ++++++ flutter/lib/native/font_manager.dart | 109 +++++++++++++++++++++++++++ flutter/lib/web/font_manager.dart | 8 ++ 4 files changed, 155 insertions(+) create mode 100644 flutter/lib/native/font_manager.dart create mode 100644 flutter/lib/web/font_manager.dart diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 58ddc0cb0..ea1907e00 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -598,6 +598,22 @@ class MyTheme { } } + /// Applies [fallbacks] as fontFamilyFallback to every text style in both + /// themes. Called once at startup on ARM64 Linux after a CJK font has been + /// loaded via FontLoader (see flutter/flutter#139293). + static void applyFontFallback(List fallbacks) { + lightTheme = lightTheme.copyWith( + textTheme: lightTheme.textTheme.apply(fontFamilyFallback: fallbacks), + primaryTextTheme: + lightTheme.primaryTextTheme.apply(fontFamilyFallback: fallbacks), + ); + darkTheme = darkTheme.copyWith( + textTheme: darkTheme.textTheme.apply(fontFamilyFallback: fallbacks), + primaryTextTheme: + darkTheme.primaryTextTheme.apply(fontFamilyFallback: fallbacks), + ); + } + static ThemeMode currentThemeMode() { final preference = getThemeModePreference(); if (preference == ThemeMode.system) { diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index 9bd68ed60..2812d6b24 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -29,6 +29,8 @@ import 'mobile/pages/home_page.dart'; import 'mobile/pages/server_page.dart'; import 'mobile/widgets/deploy_dialog.dart'; import 'models/platform_model.dart'; +import 'native/font_manager.dart' + if (dart.library.html) 'web/font_manager.dart'; import 'package:flutter_hbb/plugin/handlers.dart' if (dart.library.html) 'package:flutter_hbb/web/plugin/handlers.dart'; @@ -37,10 +39,15 @@ import 'package:flutter_hbb/plugin/handlers.dart' int? kWindowId; WindowType? kWindowType; late List kBootArgs; +bool _cjkFontLoaded = false; Future main(List args) async { earlyAssert(); WidgetsFlutterBinding.ensureInitialized(); + _cjkFontLoaded = await loadSystemCJKFonts(); + if (_cjkFontLoaded) { + MyTheme.applyFontFallback([kLinuxCjkFontFamily]); + } debugPrint("launch args: $args"); kBootArgs = List.from(args); @@ -383,6 +390,7 @@ void _runApp( builder: (context, child) { child = _keepScaleBuilder(context, child); child = botToastBuilder(context, child); + if (_cjkFontLoaded) child = _mergeCjkFallback(context, child); return child; }, ), @@ -533,6 +541,7 @@ class _AppState extends State with WidgetsBindingObserver { : (context, child) { child = _keepScaleBuilder(context, child); child = botToastBuilder(context, child); + if (_cjkFontLoaded) child = _mergeCjkFallback(context, child); if ((isDesktop && desktopType == DesktopType.main) || isWebDesktop) { child = keyListenerBuilder(context, child); @@ -586,6 +595,19 @@ _registerEventHandler() { } } +/// Merges the theme's fontFamilyFallback into [DefaultTextStyle] so that +/// bare [Text] widgets (and those with inherit:true styles) also pick up the +/// CJK fallback font loaded on ARM64 Linux. +Widget _mergeCjkFallback(BuildContext context, Widget? child) { + final result = child ?? Container(); + final fallback = Theme.of(context).textTheme.bodyMedium?.fontFamilyFallback; + if (fallback == null || fallback.isEmpty) return result; + return DefaultTextStyle.merge( + style: TextStyle(fontFamilyFallback: fallback), + child: result, + ); +} + Widget keyListenerBuilder(BuildContext context, Widget? child) { return RawKeyboardListener( focusNode: FocusNode(), diff --git a/flutter/lib/native/font_manager.dart b/flutter/lib/native/font_manager.dart new file mode 100644 index 000000000..c714b84f3 --- /dev/null +++ b/flutter/lib/native/font_manager.dart @@ -0,0 +1,109 @@ +import 'dart:ffi' show Abi; +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; + +/// Font family name registered with [FontLoader] when a system CJK font is +/// successfully loaded on ARM64 Linux. +const kLinuxCjkFontFamily = 'SystemCJK'; + +const _kFontSearchPaths = [ + // Debian / Ubuntu (noto-fonts / fonts-noto-cjk) + '/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc', + '/usr/share/fonts/truetype/noto/NotoSansCJK-Regular.ttc', + '/usr/share/fonts/opentype/noto/NotoSansCJKsc-Regular.otf', + // Fedora / RHEL / Rocky (google-noto-sans-cjk-fonts) + '/usr/share/fonts/google-noto-cjk/NotoSansCJK-Regular.ttc', + '/usr/share/fonts/google-noto-sans-cjk-fonts/NotoSansCJK-Regular.ttc', + // Arch Linux (noto-fonts-cjk) + '/usr/share/fonts/noto-cjk/NotoSansCJK-Regular.ttc', + '/usr/share/fonts/noto-cjk/NotoSansCJKsc-Regular.otf', + // Generic fallback paths + '/usr/share/fonts/noto/NotoSansCJK-Regular.ttc', + '/usr/share/fonts/noto/NotoSansCJKsc-Regular.otf', + // WenQuanYi — commonly pre-installed on CJK-locale systems + '/usr/share/fonts/truetype/wqy/wqy-microhei.ttc', + '/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc', + '/usr/share/fonts/wqy-microhei/wqy-microhei.ttc', + '/usr/share/fonts/wqy-zenhei/wqy-zenhei.ttc', +]; + +/// Loads a system CJK font on ARM64 Linux into Flutter's font registry via +/// [FontLoader], working around the missing fontconfig support in the +/// flutter-elinux engine (https://github.com/flutter/flutter/issues/139293). +/// +/// Returns true if a CJK font was successfully loaded; false otherwise. +/// On all other platforms this is a no-op and returns false immediately. +Future loadSystemCJKFonts() async { + if (Abi.current() != Abi.linuxArm64) return false; + + final path = await _findCjkFontPath(); + if (path == null) { + debugPrint('ARM64 Linux: no CJK font found; CJK text may not render'); + return false; + } + + try { + final loader = FontLoader(kLinuxCjkFontFamily); + final bytes = await File(path).readAsBytes(); + loader.addFont(Future.value(ByteData.view(bytes.buffer, bytes.offsetInBytes, bytes.lengthInBytes))); + await loader.load(); + debugPrint('ARM64 Linux: loaded CJK font from $path'); + return true; + } catch (e) { + debugPrint('ARM64 Linux: failed to load CJK font: $e'); + return false; + } +} + +Future _findCjkFontPath() async { + // Query fc-list for each CJK script separately. Fonts present in all three + // sets (zh ∩ ja ∩ ko) are true pan-CJK fonts; prefer them so we don't + // accidentally pick a Chinese-only font that lacks Japanese kana or Korean + // hangul glyphs. fc-list is a fontconfig CLI tool available on most Linux + // systems independent of whether the Flutter engine was built with fontconfig. + final byLang = >{}; + for (final lang in const ['zh', 'ja', 'ko']) { + final paths = {}; + try { + final r = + await Process.run('fc-list', [':lang=$lang', '--format=%{file}\n']); + if (r.exitCode == 0) { + for (final line in r.stdout.toString().split('\n')) { + final p = line.trim(); + if (p.isNotEmpty && File(p).existsSync()) paths.add(p); + } + } + } catch (e) { + debugPrint('ARM64 Linux: fc-list failed for lang=$lang: $e'); + } + byLang[lang] = paths; + } + + final panCjk = byLang['zh']! + .intersection(byLang['ja']!) + .intersection(byLang['ko']!); + final anyCjk = + byLang.values.fold({}, (acc, s) => acc..addAll(s)); + + // Among candidates, prefer well-known pan-CJK font families. + String? pick(Iterable pool) { + const preferred = ['notosanscjk', 'sourcehansans', 'sourcehanserif']; + for (final name in preferred) { + for (final p in pool) { + if (p.toLowerCase().contains(name)) return p; + } + } + return pool.isNotEmpty ? pool.first : null; + } + + final found = pick(panCjk) ?? pick(anyCjk); + if (found != null) return found; + + for (final p in _kFontSearchPaths) { + if (File(p).existsSync()) return p; + } + return null; +} diff --git a/flutter/lib/web/font_manager.dart b/flutter/lib/web/font_manager.dart new file mode 100644 index 000000000..c31d32890 --- /dev/null +++ b/flutter/lib/web/font_manager.dart @@ -0,0 +1,8 @@ +/// Web stub for `native/font_manager.dart`. +/// +/// The native implementation depends on `dart:io` (Process/File/Platform) to +/// load a system CJK font on ARM64 Linux, which cannot compile for the web +/// target. The web build has no such fontconfig limitation, so this is a no-op. +const kLinuxCjkFontFamily = 'SystemCJK'; + +Future loadSystemCJKFonts() async => false;