fix(subtitles): render Korean glyphs on the MPV path via bundled Hangul font

GoNotoCurrent lacks Hangul syllables, and the Android libmpv build has no
fontconfig/system-font fallback, so libass could not resolve any Korean
glyph and subtitles rendered as boxes. Ship a Hangul subset of
GoNotoKurrent-Regular beside the default font in the extracted subtitle
fonts directory; libass picks it up as fallback by glyph coverage.

close #1932
This commit is contained in:
edde746
2026-08-14 13:02:26 +02:00
parent 688bfdacb4
commit 12d9865a41
4 changed files with 80 additions and 5 deletions
Binary file not shown.
+22 -5
View File
@@ -6,11 +6,28 @@ import 'package:path_provider/path_provider.dart';
import '../utils/app_logger.dart';
/// Extracts font files from Flutter assets to the cache directory for
/// comprehensive Unicode coverage (including CJK characters) for libass subtitles.
/// comprehensive Unicode coverage (including CJK and Hangul characters) for
/// libass subtitles.
///
/// GoNotoCurrent (the primary font) covers every common script except Korean,
/// and the Android libmpv build has no fontconfig/system-font fallback, so
/// libass can only find glyphs in this directory. The Hangul subset next to
/// it provides the missing syllables: generated from GoNotoKurrent-Regular
/// (satbyy/go-noto-universal v7.0) via fontTools.subset, keeping Hangul
/// Syllables, Jamo, compat Jamo, and common Hangul punctuation.
class SubtitleFontLoader {
static const String _fontAssetPath = 'assets/go-noto-current-regular.ttf';
static const String _fontName = 'Go Noto Current-Regular';
/// Hangul-only subset of Go Noto Kurrent-Regular (see class docs).
static const String _hangulFontAssetPath = 'assets/go-noto-kurrent-hangul.ttf';
/// Font files extracted to the subtitle fonts directory, in extraction order.
static const List<(String, String)> _fonts = [
(_fontAssetPath, 'go-noto-current-regular.ttf'),
(_hangulFontAssetPath, 'go-noto-kurrent-hangul.ttf'),
];
/// In-memory cache of the resolved font directory. The filesystem work
/// (temp dir lookup, existence checks, asset extraction) is idempotent per
/// process — caching the result skips ~20ms on every subsequent Player
@@ -30,10 +47,10 @@ class SubtitleFontLoader {
await fontDir.create(recursive: true);
}
final fontFile = File(path.join(fontDir.path, 'go-noto-current-regular.ttf'));
if (!await fontFile.exists()) {
final fontData = await rootBundle.load(_fontAssetPath);
for (final (assetPath, fileName) in _fonts) {
final fontFile = File(path.join(fontDir.path, fileName));
if (await fontFile.exists()) continue;
final fontData = await rootBundle.load(assetPath);
await fontFile.writeAsBytes(fontData.buffer.asUint8List());
}
+1
View File
@@ -146,6 +146,7 @@ flutter:
- assets/seerr_mark.svg
- assets/mdblist_mark.svg
- assets/go-noto-current-regular.ttf
- assets/go-noto-kurrent-hangul.ttf
- assets/shaders/nvscaler/
- assets/shaders/artcnn/
- assets/shaders/anime4k/
+57
View File
@@ -0,0 +1,57 @@
import 'dart:io';
import 'dart:typed_data';
import 'package:flutter_test/flutter_test.dart';
import 'package:path/path.dart' as p;
import 'package:path_provider_platform_interface/path_provider_platform_interface.dart';
import 'package:plezy/mpv/font_loader.dart';
import '../test_helpers/io_fakes.dart';
/// Guards the subtitle-font extraction contract: libass on Android MPV has no
/// fontconfig/system fallback, so every script must ship inside the one
/// directory this loader produces. Losing a font here silently regresses
/// glyph coverage for subtitles (e.g. #1932, Korean Hangul).
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
late Directory tempRoot;
late PathProviderPlatform originalPlatform;
setUp(() {
originalPlatform = PathProviderPlatform.instance;
tempRoot = Directory.systemTemp.createTempSync('font_loader_test');
PathProviderPlatform.instance = FakePathProvider(tempRoot);
});
tearDown(() {
PathProviderPlatform.instance = originalPlatform;
tempRoot.deleteSync(recursive: true);
});
// loadSubtitleFont() caches its result per process, so only this test may
// trigger extraction.
test('extracts the default and Hangul fonts into one directory', () async {
final fontDir = await SubtitleFontLoader.loadSubtitleFont();
expect(fontDir, isNotNull);
final dir = Directory(fontDir!);
expect(dir.existsSync(), isTrue);
for (final fileName in ['go-noto-current-regular.ttf', 'go-noto-kurrent-hangul.ttf']) {
final fontFile = File(p.join(fontDir, fileName));
expect(fontFile.existsSync(), isTrue, reason: '$fileName missing');
final bytes = fontFile.readAsBytesSync();
expect(bytes.length, greaterThan(0), reason: '$fileName empty');
// TrueType magic: 0x00010000. Catches a corrupt/truncated asset.
expect(bytes.sublist(0, 4), Uint8List.fromList([0x00, 0x01, 0x00, 0x00]));
}
});
test('default font name stays Go Noto Current-Regular', () {
// configureSubtitleFonts() sets sub-font to this exact name; libass falls
// back to the Hangul subset for Korean glyphs.
expect(SubtitleFontLoader.fontName, 'Go Noto Current-Regular');
});
}