diff --git a/lib/web_ui/lib/src/engine/canvaskit/fonts.dart b/lib/web_ui/lib/src/engine/canvaskit/fonts.dart index e2e8e2804fe40..560577e681d26 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/fonts.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/fonts.dart @@ -24,12 +24,29 @@ const String _ahemUrl = '/assets/fonts/ahem.ttf'; /// Manages the fonts used in the Skia-based backend. class SkiaFontCollection { - /// Fonts that have been registered but haven't been loaded yet. - final List> _unloadedFonts = - >[]; + final Set _registeredFontFamilies = {}; - /// Fonts which have been registered and loaded. - final List _registeredFonts = []; + /// Fonts that started the download process. + /// + /// Once downloaded successfully, this map is cleared and the resulting + /// [RegisteredFont]s are added to [_downloadedFonts]. + final List> _pendingFonts = >[]; + + /// Fonts that have been downloaded and parsed into [SkTypeface]. + /// + /// These fonts may not yet have been registered with the [fontProvider]. This + /// happens after [ensureFontsLoaded] completes. + final List _downloadedFonts = []; + + /// Returns fonts that have been downloaded and parsed. + /// + /// This should only be used in tests. + List? get debugDownloadedFonts { + if (!assertionsEnabled) { + return null; + } + return _downloadedFonts; + } final Map> familyToFontMap = >{}; @@ -43,7 +60,7 @@ class SkiaFontCollection { fontProvider = canvasKit.TypefaceFontProvider.Make(); familyToFontMap.clear(); - for (final RegisteredFont font in _registeredFonts) { + for (final RegisteredFont font in _downloadedFonts) { fontProvider!.registerFont(font.bytes, font.family); familyToFontMap .putIfAbsent(font.family, () => []) @@ -59,19 +76,19 @@ class SkiaFontCollection { } } - /// Loads all of the unloaded fonts in [_unloadedFonts] and adds them - /// to [_registeredFonts]. + /// Loads all of the unloaded fonts in [_pendingFonts] and adds them + /// to [_downloadedFonts]. Future _loadFonts() async { - if (_unloadedFonts.isEmpty) { + if (_pendingFonts.isEmpty) { return; } - final List loadedFonts = await Future.wait(_unloadedFonts); + final List loadedFonts = await Future.wait(_pendingFonts); for (final RegisteredFont? font in loadedFonts) { if (font != null) { - _registeredFonts.add(font); + _downloadedFonts.add(font); } } - _unloadedFonts.clear(); + _pendingFonts.clear(); } Future loadFontFromList(Uint8List list, {String? fontFamily}) async { @@ -86,7 +103,7 @@ class SkiaFontCollection { final SkTypeface? typeface = canvasKit.Typeface.MakeFreeTypeFaceFromData(list.buffer); if (typeface != null) { - _registeredFonts.add(RegisteredFont(list, fontFamily, typeface)); + _downloadedFonts.add(RegisteredFont(list, fontFamily, typeface)); await ensureFontsLoaded(); } else { printWarning('Failed to parse font family "$fontFamily"'); @@ -94,6 +111,7 @@ class SkiaFontCollection { } } + /// Loads fonts from `FontManifest.json`. Future registerFonts(AssetManager assetManager) async { ByteData byteData; @@ -115,61 +133,75 @@ class SkiaFontCollection { 'There was a problem trying to load FontManifest.json'); } - bool registeredRoboto = false; - for (final Map fontFamily in fontManifest.cast>()) { final String family = fontFamily.readString('family'); final List fontAssets = fontFamily.readList('fonts'); - - if (family == 'Roboto') { - registeredRoboto = true; - } - for (final dynamic fontAssetItem in fontAssets) { final Map fontAsset = fontAssetItem as Map; final String asset = fontAsset.readString('asset'); - _unloadedFonts - .add(_registerFont(assetManager.getAssetUrl(asset), family)); + _registerFont(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 (!registeredRoboto) { + if (!_isFontFamilyRegistered('Roboto')) { // Download Roboto and add it to the font buffers. - _unloadedFonts.add(_registerFont(_robotoUrl, 'Roboto')); + _registerFont(_robotoUrl, 'Roboto'); } } - Future debugRegisterTestFonts() async { - _unloadedFonts.add(_registerFont(_ahemUrl, 'Ahem')); - FontFallbackData.instance.globalFontFallbacks.add('Ahem'); + /// Whether the [fontFamily] was registered and/or loaded. + bool _isFontFamilyRegistered(String fontFamily) { + return _registeredFontFamilies.contains(fontFamily); } - Future _registerFont(String url, String family) async { - ByteBuffer buffer; - try { - buffer = await httpFetch(url).then(_getArrayBuffer); - } catch (e) { - printWarning('Failed to load font $family at $url'); - printWarning(e.toString()); - return null; + /// Loads the Ahem font, unless it's already been loaded using + /// `FontManifest.json` (see [registerFonts]). + /// + /// `FontManifest.json` has higher priority than the default test font URLs. + /// This allows customizing test environments where fonts are loaded from + /// different URLs. + void debugRegisterTestFonts() { + if (!_isFontFamilyRegistered('Ahem')) { + _registerFont(_ahemUrl, 'Ahem'); } - 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; + // Ahem must be added to font fallbacks list regardless of where it was + // downloaded from. + FontFallbackData.instance.globalFontFallbacks.add('Ahem'); + } + + void _registerFont(String url, String family) { + Future _downloadFont() async { + ByteBuffer buffer; + try { + buffer = await httpFetch(url).then(_getArrayBuffer); + } catch (e) { + printWarning('Failed to load font $family at $url'); + printWarning(e.toString()); + return null; + } + + 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; + } } + + _registeredFontFamilies.add(family); + _pendingFonts.add(_downloadFont()); } + String? _readActualFamilyName(Uint8List bytes) { final SkFontMgr tmpFontMgr = canvasKit.FontMgr.FromData([bytes])!; diff --git a/lib/web_ui/test/canvaskit/skia_font_collection_test.dart b/lib/web_ui/test/canvaskit/skia_font_collection_test.dart index aacdafe7e10a6..7ef8221c0b0da 100644 --- a/lib/web_ui/test/canvaskit/skia_font_collection_test.dart +++ b/lib/web_ui/test/canvaskit/skia_font_collection_test.dart @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:typed_data'; + import 'package:test/bootstrap/browser.dart'; import 'package:test/test.dart'; @@ -72,6 +74,57 @@ void testMain() { ), ); }); + + test('prioritizes Ahem loaded via FontManifest.json', () async { + final SkiaFontCollection fontCollection = SkiaFontCollection(); + final WebOnlyMockAssetManager mockAssetManager = + WebOnlyMockAssetManager(); + mockAssetManager.defaultFontManifest = ''' + [ + { + "family":"Ahem", + "fonts":[{"asset":"/assets/fonts/Roboto-Regular.ttf"}] + } + ] + '''.trim(); + + final ByteBuffer robotoData = (await (await httpFetch('/assets/fonts/Roboto-Regular.ttf')).arrayBuffer())! as ByteBuffer; + + await fontCollection.registerFonts(mockAssetManager); + fontCollection.debugRegisterTestFonts(); + await fontCollection.ensureFontsLoaded(); + expect(warnings, isEmpty); + + // Use `singleWhere` to make sure only one version of 'Ahem' is loaded. + final RegisteredFont ahem = fontCollection.debugDownloadedFonts! + .singleWhere((RegisteredFont font) => font.family == 'Ahem'); + + // Check that the contents of 'Ahem' is actually Roboto, because that's + // what's specified in the manifest, and the manifest takes precedence. + expect(ahem.bytes.length, robotoData.lengthInBytes); + }); + + test('falls back to default Ahem URL', () async { + final SkiaFontCollection fontCollection = SkiaFontCollection(); + final WebOnlyMockAssetManager mockAssetManager = + WebOnlyMockAssetManager(); + mockAssetManager.defaultFontManifest = '[]'; + + final ByteBuffer ahemData = (await (await httpFetch('/assets/fonts/ahem.ttf')).arrayBuffer())! as ByteBuffer; + + await fontCollection.registerFonts(mockAssetManager); + fontCollection.debugRegisterTestFonts(); + await fontCollection.ensureFontsLoaded(); + expect(warnings, isEmpty); + + // Use `singleWhere` to make sure only one version of 'Ahem' is loaded. + final RegisteredFont ahem = fontCollection.debugDownloadedFonts! + .singleWhere((RegisteredFont font) => font.family == 'Ahem'); + + // Check that the contents of 'Ahem' is actually Roboto, because that's + // what's specified in the manifest, and the manifest takes precedence. + expect(ahem.bytes.length, ahemData.lengthInBytes); + }); // TODO(hterkelsen): https://github.com/flutter/flutter/issues/60040 }, skip: isIosSafari); }