mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-10-22 15:28:45 +02:00
97 lines
3.2 KiB
Dart
97 lines
3.2 KiB
Dart
import 'dart:io';
|
|
|
|
import 'package:crypto/crypto.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:path_provider/path_provider.dart';
|
|
|
|
/// Copies imported fonts into app storage; never persists a picker/container path.
|
|
class CustomFontService {
|
|
CustomFontService({
|
|
Future<Directory> Function()? directory,
|
|
Future<void> Function(String, Uint8List)? loadFont,
|
|
}) : _directory = directory ?? _fontDirectory,
|
|
_loadFont = loadFont ?? _register;
|
|
|
|
static const maxBytes = 10 * 1024 * 1024;
|
|
static final instance = CustomFontService();
|
|
final Future<Directory> Function() _directory;
|
|
final Future<void> Function(String, Uint8List) _loadFont;
|
|
final Map<String, Future<String>> _loads = {};
|
|
static final _idPattern = RegExp(r'^[a-f0-9]{64}$');
|
|
|
|
static Future<Directory> _fontDirectory() async => Directory(
|
|
'${(await getApplicationSupportDirectory()).path}/custom_fonts',
|
|
).create(recursive: true);
|
|
|
|
static Future<void> _register(String family, Uint8List bytes) async {
|
|
final loader = FontLoader(family)
|
|
..addFont(Future.value(ByteData.sublistView(bytes)));
|
|
await loader.load();
|
|
}
|
|
|
|
static void validate(Uint8List bytes) {
|
|
if (bytes.length < 12 || bytes.length > maxBytes) {
|
|
throw const FormatException('Invalid font size');
|
|
}
|
|
final data = ByteData.sublistView(bytes);
|
|
final signature = data.getUint32(0);
|
|
if (signature != 0x00010000 && signature != 0x4f54544f) {
|
|
throw const FormatException('Expected a TTF or OTF font');
|
|
}
|
|
final count = data.getUint16(4);
|
|
if (count == 0 || 12 + count * 16 > bytes.length) {
|
|
throw const FormatException('Invalid font table directory');
|
|
}
|
|
for (var i = 0; i < count; i++) {
|
|
final table = 12 + i * 16;
|
|
final offset = data.getUint32(table + 8);
|
|
final length = data.getUint32(table + 12);
|
|
if (offset > bytes.length || length > bytes.length - offset) {
|
|
throw const FormatException('Truncated font table');
|
|
}
|
|
}
|
|
}
|
|
|
|
Future<String> import(Uint8List bytes) async {
|
|
validate(bytes);
|
|
final id = sha256.convert(bytes).toString();
|
|
await _load(id, bytes);
|
|
final directory = await _directory();
|
|
final destination = File('${directory.path}/$id.font');
|
|
if (!await destination.exists()) {
|
|
final staged = await File(
|
|
'${directory.path}/$id.tmp',
|
|
).writeAsBytes(bytes, flush: true);
|
|
await staged.rename(destination.path);
|
|
}
|
|
return id;
|
|
}
|
|
|
|
Future<String> _load(String id, Uint8List bytes) =>
|
|
_loads.putIfAbsent(id, () async {
|
|
final family = 'ImportedFont_$id';
|
|
try {
|
|
await _loadFont(family, bytes);
|
|
return family;
|
|
} catch (_) {
|
|
_loads.remove(id);
|
|
rethrow;
|
|
}
|
|
});
|
|
|
|
Future<String?> restore(String id) async {
|
|
if (!_idPattern.hasMatch(id)) return null;
|
|
try {
|
|
final file = File('${(await _directory()).path}/$id.font');
|
|
if (await file.length() > maxBytes) return null;
|
|
final bytes = await file.readAsBytes();
|
|
validate(bytes);
|
|
if (sha256.convert(bytes).toString() != id) return null;
|
|
return await _load(id, bytes);
|
|
} catch (_) {
|
|
// Missing fonts after restoring preferences fall back to the theme font.
|
|
return null;
|
|
}
|
|
}
|
|
}
|