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:
Binary file not shown.
@@ -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());
|
||||
}
|
||||
|
||||
|
||||
@@ -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/
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user