blob: 86c5ab5e2edf79bee35ac2a6de307baf705967df [file] [log] [blame]
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
import 'dart:convert';
import 'dart:typed_data';
import 'package:web_test_fonts/web_test_fonts.dart';
import '../assets.dart';
import '../dom.dart';
import '../fonts.dart';
import '../util.dart';
import 'canvaskit_api.dart';
import 'font_fallbacks.dart';
// This URL was found by using the Google Fonts Developer API to find the URL
// for Roboto. The API warns that this URL is not stable. In order to update
// this, list out all of the fonts and find the URL for the regular
// Roboto font. The API reference is here:
// https://developers.google.com/fonts/docs/developer_api
const String _robotoUrl =
'https://fonts.gstatic.com/s/roboto/v20/KFOmCnqEu92Fr1Me5WZLCzYlKw.ttf';
/// Manages the fonts used in the Skia-based backend.
class SkiaFontCollection implements FlutterFontCollection {
final Set<String> _downloadedFontFamilies = <String>{};
/// Fonts that started the download process, but are not yet registered.
///
/// /// Once downloaded successfully, this map is cleared and the resulting
/// [UnregisteredFont]s are added to [_registeredFonts].
final List<UnregisteredFont> _unregisteredFonts = <UnregisteredFont>[];
final List<RegisteredFont> _registeredFonts = <RegisteredFont>[];
/// Returns fonts that have been downloaded, registered, and parsed.
///
/// This should only be used in tests.
List<RegisteredFont>? get debugRegisteredFonts {
if (!assertionsEnabled) {
return null;
}
return _registeredFonts;
}
final Map<String, List<SkFont>> familyToFontMap = <String, List<SkFont>>{};
void _registerWithFontProvider() {
if (_fontProvider != null) {
_fontProvider!.delete();
_fontProvider = null;
skFontCollection?.delete();
skFontCollection = null;
}
_fontProvider = canvasKit.TypefaceFontProvider.Make();
skFontCollection = canvasKit.FontCollection.Make();
skFontCollection!.enableFontFallback();
skFontCollection!.setDefaultFontManager(_fontProvider);
familyToFontMap.clear();
for (final RegisteredFont font in _registeredFonts) {
_fontProvider!.registerFont(font.bytes, font.family);
familyToFontMap
.putIfAbsent(font.family, () => <SkFont>[])
.add(SkFont(font.typeface));
}
for (final RegisteredFont font
in FontFallbackData.instance.registeredFallbackFonts) {
_fontProvider!.registerFont(font.bytes, font.family);
familyToFontMap
.putIfAbsent(font.family, () => <SkFont>[])
.add(SkFont(font.typeface));
}
}
@override
Future<void> loadFontFromList(Uint8List list, {String? fontFamily}) async {
if (fontFamily == null) {
fontFamily = _readActualFamilyName(list);
if (fontFamily == null) {
printWarning('Failed to read font family name. Aborting font load.');
return;
}
}
final SkTypeface? typeface =
canvasKit.Typeface.MakeFreeTypeFaceFromData(list.buffer);
if (typeface != null) {
_registeredFonts.add(RegisteredFont(list, fontFamily, typeface));
_registerWithFontProvider();
} else {
printWarning('Failed to parse font family "$fontFamily"');
return;
}
}
/// Loads fonts from `FontManifest.json`.
@override
Future<void> downloadAssetFonts(AssetManager assetManager) async {
final HttpFetchResponse response = await assetManager.loadAsset('FontManifest.json');
if (!response.hasPayload) {
printWarning('Font manifest does not exist at `${response.url}` - ignoring.');
return;
}
final Uint8List data = await response.asUint8List();
final List<dynamic>? fontManifest = json.decode(utf8.decode(data)) as List<dynamic>?;
if (fontManifest == null) {
throw AssertionError(
'There was a problem trying to load FontManifest.json');
}
final List<Future<UnregisteredFont?>> pendingFonts = <Future<UnregisteredFont?>>[];
for (final Map<String, dynamic> fontFamily
in fontManifest.cast<Map<String, dynamic>>()) {
final String family = fontFamily.readString('family');
final List<dynamic> fontAssets = fontFamily.readList('fonts');
for (final dynamic fontAssetItem in fontAssets) {
final Map<String, dynamic> fontAsset = fontAssetItem as Map<String, dynamic>;
final String asset = fontAsset.readString('asset');
_downloadFont(pendingFonts, assetManager.getAssetUrl(asset), family);
}
}
/// We need a default fallback font for CanvasKit, in order to
/// avoid crashing while laying out text with an unregistered font. We chose
/// Roboto to match Android.
if (!_isFontFamilyDownloaded('Roboto')) {
// Download Roboto and add it to the font buffers.
_downloadFont(pendingFonts, _robotoUrl, 'Roboto');
}
final List<UnregisteredFont?> completedPendingFonts = await Future.wait(pendingFonts);
_unregisteredFonts.addAll(completedPendingFonts.whereType<UnregisteredFont>());
}
@override
void registerDownloadedFonts() {
RegisteredFont? makeRegisterFont(ByteBuffer buffer, String url, String family) {
final Uint8List bytes = buffer.asUint8List();
final SkTypeface? typeface =
canvasKit.Typeface.MakeFreeTypeFaceFromData(bytes.buffer);
if (typeface != null) {
return RegisteredFont(bytes, family, typeface);
} else {
printWarning('Failed to load font $family at $url');
printWarning('Verify that $url contains a valid font.');
return null;
}
}
for (final UnregisteredFont unregisteredFont in _unregisteredFonts) {
final RegisteredFont? registeredFont = makeRegisterFont(
unregisteredFont.bytes,
unregisteredFont.url,
unregisteredFont.family
);
if (registeredFont != null) {
_registeredFonts.add(registeredFont);
}
}
_unregisteredFonts.clear();
_registerWithFontProvider();
}
/// Whether the [fontFamily] was registered and/or loaded.
bool _isFontFamilyDownloaded(String fontFamily) {
return _downloadedFontFamilies.contains(fontFamily);
}
/// Loads the Ahem font, unless it's already been loaded using
/// `FontManifest.json` (see [downloadAssetFonts]).
///
/// `FontManifest.json` has higher priority than the default test font URLs.
/// This allows customizing test environments where fonts are loaded from
/// different URLs.
@override
Future<void> debugDownloadTestFonts() async {
final List<Future<UnregisteredFont?>> pendingFonts = <Future<UnregisteredFont?>>[];
for (final MapEntry<String, String> fontEntry in testFontUrls.entries) {
if (!_isFontFamilyDownloaded(fontEntry.key)) {
_downloadFont(pendingFonts, fontEntry.value, fontEntry.key);
}
}
final List<UnregisteredFont?> completedPendingFonts = await Future.wait(pendingFonts);
final List<UnregisteredFont> fonts = <UnregisteredFont>[
UnregisteredFont(
EmbeddedTestFont.flutterTest.data.buffer,
'<embedded>',
EmbeddedTestFont.flutterTest.fontFamily,
),
...completedPendingFonts.whereType<UnregisteredFont>(),
];
_unregisteredFonts.addAll(fonts);
// Ahem must be added to font fallbacks list regardless of where it was
// downloaded from.
FontFallbackData.instance.globalFontFallbacks.add(ahemFontFamily);
}
void _downloadFont(
List<Future<UnregisteredFont?>> waitUnregisteredFonts,
String url,
String family
) {
Future<UnregisteredFont?> downloadFont() async {
// Try to get the font leniently. Do not crash the app when failing to
// fetch the font in the spirit of "gradual degradation of functionality".
try {
final ByteBuffer data = await httpFetchByteBuffer(url);
return UnregisteredFont(data, url, family);
} catch (e) {
printWarning('Failed to load font $family at $url');
printWarning(e.toString());
return null;
}
}
_downloadedFontFamilies.add(family);
waitUnregisteredFonts.add(downloadFont());
}
String? _readActualFamilyName(Uint8List bytes) {
final SkFontMgr tmpFontMgr =
canvasKit.FontMgr.FromData(<Uint8List>[bytes])!;
final String? actualFamily = tmpFontMgr.getFamilyName(0);
tmpFontMgr.delete();
return actualFamily;
}
TypefaceFontProvider? _fontProvider;
SkFontCollection? skFontCollection;
@override
void clear() {}
}
/// Represents a font that has been registered.
class RegisteredFont {
RegisteredFont(this.bytes, this.family, this.typeface) {
// This is a hack which causes Skia to cache the decoded font.
final SkFont skFont = SkFont(typeface);
skFont.getGlyphBounds(<int>[0], null, null);
}
/// The font family name for this font.
final String family;
/// The byte data for this font.
final Uint8List bytes;
/// The [SkTypeface] created from this font's [bytes].
///
/// This is used to determine which code points are supported by this font.
final SkTypeface typeface;
}
/// Represents a font that has been downloaded but not registered.
class UnregisteredFont {
const UnregisteredFont(this.bytes, this.url, this.family);
final ByteBuffer bytes;
final String url;
final String family;
}