diff --git a/assets/go-noto-kurrent-hangul.ttf b/assets/go-noto-kurrent-hangul.ttf new file mode 100644 index 000000000..9886e845a Binary files /dev/null and b/assets/go-noto-kurrent-hangul.ttf differ diff --git a/lib/mpv/font_loader.dart b/lib/mpv/font_loader.dart index 333cd2b7d..4b20a1833 100644 --- a/lib/mpv/font_loader.dart +++ b/lib/mpv/font_loader.dart @@ -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()); } diff --git a/pubspec.yaml b/pubspec.yaml index 88260cf19..68cdce43f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -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/ diff --git a/test/mpv/font_loader_test.dart b/test/mpv/font_loader_test.dart new file mode 100644 index 000000000..deb090b7c --- /dev/null +++ b/test/mpv/font_loader_test.dart @@ -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'); + }); +}