part of dart._engine; // 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. // 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 String _robotoUrl = '${configuration.fontFallbackBaseUrl}roboto/v32/KFOmCnqEu92Fr1Me4GZLCzYlKw.woff2'; /// Manages the fonts used in the Skia-based backend. class SkiaFontCollection implements FlutterFontCollection { final Set _downloadedFontFamilies = {}; @override late FontFallbackManager fontFallbackManager = FontFallbackManager(SkiaFallbackRegistry(this)); /// 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 _unregisteredFonts = []; final List _registeredFonts = []; final List registeredFallbackFonts = []; /// Returns fonts that have been downloaded, registered, and parsed. /// /// This should only be used in tests. List? get debugRegisteredFonts { List? result; assert(() { result = _registeredFonts; return true; }()); return result; } final Map> familyToFontMap = >{}; 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, () => []).add(SkFont(font.typeface)); } for (final RegisteredFont font in registeredFallbackFonts) { _fontProvider!.registerFont(font.bytes, font.family); familyToFontMap.putIfAbsent(font.family, () => []).add(SkFont(font.typeface)); } } @override Future 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 false; } } // Make sure CanvasKit is actually loaded await renderer.initialize(); 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 false; } return true; } /// Loads fonts from `FontManifest.json`. @override Future loadAssetFonts(FontManifest manifest) async { final List> pendingDownloads = >[]; bool loadedRoboto = false; for (final FontFamily family in manifest.families) { if (family.name == 'Roboto') { loadedRoboto = true; } for (final FontAsset fontAsset in family.fontAssets) { final String url = ui_web.assetManager.getAssetUrl(fontAsset.asset); pendingDownloads.add(_downloadFont(fontAsset.asset, url, family.name)); } } /// 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 (!loadedRoboto) { // Download Roboto and add it to the font buffers. pendingDownloads.add(_downloadFont('Roboto', _robotoUrl, 'Roboto')); } final Map fontFailures = {}; final List<(String, UnregisteredFont)> downloadedFonts = <(String, UnregisteredFont)>[]; for (final FontDownloadResult result in await Future.wait(pendingDownloads)) { if (result.font != null) { downloadedFonts.add((result.assetName, result.font!)); } else { fontFailures[result.assetName] = result.error!; } } // Make sure CanvasKit is actually loaded await renderer.initialize(); final List loadedFonts = []; for (final (String assetName, UnregisteredFont unregisteredFont) in downloadedFonts) { final Uint8List bytes = unregisteredFont.bytes.asUint8List(); final SkTypeface? typeface = canvasKit.Typeface.MakeFreeTypeFaceFromData(bytes.buffer); if (typeface != null) { loadedFonts.add(assetName); _registeredFonts.add(RegisteredFont(bytes, unregisteredFont.family, typeface)); } else { printWarning('Failed to load font ${unregisteredFont.family} at ${unregisteredFont.url}'); printWarning('Verify that ${unregisteredFont.url} contains a valid font.'); fontFailures[assetName] = FontInvalidDataError(unregisteredFont.url); } } registerDownloadedFonts(); return AssetFontsResult(loadedFonts, fontFailures); } 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(); } Future _downloadFont(String assetName, String url, String fontFamily) async { final ByteBuffer fontData; // 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 HttpFetchResponse response = await httpFetch(url); if (!response.hasPayload) { printWarning('Font family $fontFamily not found (404) at $url'); return FontDownloadResult.fromError(assetName, FontNotFoundError(url)); } fontData = await response.asByteBuffer(); } catch (e) { printWarning('Failed to load font $fontFamily at $url'); printWarning(e.toString()); return FontDownloadResult.fromError(assetName, FontDownloadError(url, e)); } _downloadedFontFamilies.add(fontFamily); return FontDownloadResult.fromFont(assetName, UnregisteredFont(fontData, url, fontFamily)); } String? _readActualFamilyName(Uint8List bytes) { final SkFontMgr tmpFontMgr = canvasKit.FontMgr.FromData([bytes])!; final String? actualFamily = tmpFontMgr.getFamilyName(0); tmpFontMgr.delete(); return actualFamily; } TypefaceFontProvider? _fontProvider; SkFontCollection? skFontCollection; @override void clear() {} @override void debugResetFallbackFonts() { fontFallbackManager = FontFallbackManager(SkiaFallbackRegistry(this)); registeredFallbackFonts.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([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; } class FontDownloadResult { FontDownloadResult.fromFont(this.assetName, UnregisteredFont this.font) : error = null; FontDownloadResult.fromError(this.assetName, FontLoadError this.error) : font = null; final String assetName; final UnregisteredFont? font; final FontLoadError? error; } class SkiaFallbackRegistry implements FallbackFontRegistry { SkiaFallbackRegistry(this.fontCollection); SkiaFontCollection fontCollection; @override List getMissingCodePoints(List codeUnits, List fontFamilies) { final List fonts = []; for (final String font in fontFamilies) { final List? typefacesForFamily = fontCollection.familyToFontMap[font]; if (typefacesForFamily != null) { fonts.addAll(typefacesForFamily); } } final List codePointsSupported = List.filled(codeUnits.length, false); final String testString = String.fromCharCodes(codeUnits); for (final SkFont font in fonts) { final Uint16List glyphs = font.getGlyphIDs(testString); assert(glyphs.length == codePointsSupported.length); for (int i = 0; i < glyphs.length; i++) { codePointsSupported[i] |= glyphs[i] != 0; } } final List missingCodeUnits = []; for (int i = 0; i < codePointsSupported.length; i++) { if (!codePointsSupported[i]) { missingCodeUnits.add(codeUnits[i]); } } return missingCodeUnits; } @override Future loadFallbackFont(String familyName, String url) async { final ByteBuffer buffer = await httpFetchByteBuffer(url); final SkTypeface? typeface = canvasKit.Typeface.MakeFreeTypeFaceFromData(buffer); if (typeface == null) { printWarning('Failed to parse fallback font $familyName as a font.'); return; } fontCollection.registeredFallbackFonts.add( RegisteredFont(buffer.asUint8List(), familyName, typeface), ); } @override void updateFallbackFontFamilies(List families) { fontCollection.registerDownloadedFonts(); } }